← blog

Beginners' Guide to Effortless Doctests in Python

Doctests are essentially tests embedded in a docstring. They serve both as example use cases and test cases! A Python expression is provided along with an expected outcome, a test runner collects that and evaluates the expression.

Getting started

Let's take a look at a run-of-the-mill docstring.

def hello(name):
    """
    Returns a greeting message saying Hello.
    :param name: str
    :return: str
    """
    return 'Hello, ' + name

It's a normal docstring with nothing too special. Let's try to get the doctest module to test our test-less function.

$ python -m doctest -v hello.py

2 items had no tests: hello hello.hello 0 tests in 2 items. 0 passed and 0 failed. Test passed.

Well, the output says it clear that we have no tests. Now, let's add a doctest. Remember how the Python Shell or REPL works? Remember how you have three arrows indicating input? Yes. Just copy that style.

def hello(name):
    """
    Returns a greeting message saying Hello.
    :param name: str
    :return: str
>>> hello('Anam')
'Hello, Anam'
"""
return 'Hello, ' + name

Time to run the doctest module again.

$ python -m doctest -v hello.py

Trying: hello(‘Anam’) Expecting: ‘Hello, Anam’ ok 1 items had no tests: hello 1 items passed all tests: 1 tests in hello.hello 1 tests in 2 items. 1 passed and 0 failed. Test passed.

Ah, that looks more like it.

Expecting Exceptions

Let's try something more fun. Let's add a case in our function, if the provided name is just composed of digits, we refuse to greet digits!

def hello(name):
    """
    Returns a greeting message saying Hello.
    :param name: str
    :return: str
>>> hello('Anam')
'Hello, Anam'
"""

if name.isdigit():
    raise ValueError('We do not greet numbers')

return 'Hello, ' + name</pre>

Now, how do we test for exceptions? Well, remember the mantra.

Replicate your shell; success you will be showered with upon you.

So, just replicate your shell! Let's see how the shell reacts to digits.

So, in the doctest,

def hello(name):
    """
    Returns a greeting message saying Hello.
    :param name: str
    :return: str
>>> hello('Anam')
'Hello, Anam'
>>> hello('121')
Traceback (most recent call last):
...
ValueError: We do not greet numbers
"""

if name.isdigit():
    raise ValueError('We do not greet numbers')

return 'Hello, ' + name</pre>

Just the first line and the last line of the exception. Don't confuse doctest with a complete, well-formatted and fully laid out stack trace, please! And just three dots to indicate there were more to it. So, let's run this again.

$ python -m doctest -v hello.py

Trying: hello(‘Anam’) Expecting: ‘Hello, Anam’ ok Trying: hello(‘121’) Expecting: Traceback (most recent call last): … ValueError: We do not greet numbers ok 1 items had no tests: hello 1 items passed all tests: 2 tests in hello.hello 2 tests in 2 items. 2 passed and 0 failed. Test passed.

And yes.

In Production

Of course, in production, you do need to use a test runner. Something in the category of pytest or nose. I personally prefer pytest. Let's take a look at configuring pytest for this.

Not so surprisingly, it discovered no tests in the default configuration. We need to add a simple flag to it --doctest-modules.

That seems to be working, let's try to intentionally run our tests to check if it is really working.

And that is working perfectly. To avoid putting the --doctest-modules flag all the time, consider making a tox.ini file to place your pytest configuration. More on this in the pytest documentation.