Skip to Content
Course content

341: Writing and Running Doctests

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen to dozens of developers. You write a handy little utility function, you add a few examples in the docstring to show how it works, and then you run the doctest module only to be met with a wall of failure. You look at the code, look at the expected output, and they look identical. It's infuriating.

def trim_and_capitalize(text):
    """
    Removes leading/trailing whitespace and capitalizes the first letter.
    
    >>> trim_and_capitalize("  hello world  ")
    'Hello world' 
    """
    return text.strip().capitalize()

if __name__ == "__main__":
    import doctest
    doctest.testmod()

If you run this, Python will tell you the test failed. It will show you that it expected 'Hello world' but got 'Hello world'. If you look closely at the code above, I've hidden a trailing space after the closing quote in the docstring's expected output. To the naked eye, it looks fine. To doctest, it's a total mismatch.

The Invisible Character Trap

The core issue here is that doctest isn't just checking if your function works; it's performing a literal string comparison between what your function returns (represented as a string) and what you typed in the docstring. It's incredibly pedantic. A single misplaced space, a missing newline, or even a difference in how a list is represented (single quotes vs. double quotes) will trigger a failure.

When you see a failure, don't just stare at the code. Look at the "Diff" provided by the output. Python will usually show you a + and a - line. The - is what the function actually produced, and the + is what you told the test to expect. If they look the same, highlight the text with your mouse to see if there are trailing spaces hiding at the end of the line.

Aligning Expectations with Reality

To fix the bug, we have to ensure the docstring is a mirror image of the Python REPL output. I usually recommend writing the function first, running it manually in a terminal, and then copy-pasting the result directly into the docstring. That way, you aren't guessing how Python formats the output.

def trim_and_capitalize(text):
    """
    Removes leading/trailing whitespace and capitalizes the first letter.
    
    >>> trim_and_capitalize("  hello world  ")
    'Hello world'
    """
    return text.strip().capitalize()

Now, the trailing space is gone. The test passes. It seems trivial, but this "exact match" requirement is why many people give up on doctests. The trick is to treat your docstrings as code, not just comments.

Running Tests Without Boilerplate

In the first example, I added the if __name__ == "__main__": block. That's fine for a quick script, but in a professional project, I don't want test-execution logic cluttering up my production modules. You can run doctests directly from your shell instead.

Try running this in your terminal:

python -m doctest my_script.py

By default, this command is silent if everything passes. If you want to see the detailed results (the "verbose" mode), add the -v flag:

python -m doctest -v my_script.py

I prefer the verbose flag during development because it gives me a satisfying confirmation that every single example was actually executed and passed, rather than just hoping for the best because there were no errors.

Choosing Doctests Over Unit Tests

You might be wondering why you'd bother with this when we have unittest or pytest. Here is my rule of thumb: use doctests for documentation and unit tests for verification.

  • Doctests are great for "happy path" examples. They tell the user, "If you give the function X, you will get Y." They serve as live documentation that can't go out of date.
  • Unit Tests are for the "sad paths." Edge cases, error handling, and complex logic belong in a separate test file where you have more powerful tools than simple string comparison.

If you find yourself writing 20 different doctests for one function, you're using the wrong tool. Move the edge cases to a test suite and keep the docstring clean and illustrative.




📋 Practical Task

Building a Validated Email Domain Extractor

Your task is to create a utility function called extract_domain that takes an email address string and returns only the domain part (everything after the '@' symbol). However, the function should return None if the email is invalid (i.e., it doesn't contain exactly one '@' symbol).

To complete this exercise, you must:

  1. Implement the extract_domain logic.
  2. Write three doctests within the function's docstring:
    • One for a standard email (e.g., "user@example.com" should return 'example.com').
    • One for an email with no '@' symbol (should return None).
    • One for an invalid email with multiple '@' symbols (should return None).
  3. Ensure the doctests pass exactly when run with the doctest module.

Note: Pay close attention to the representation of None in your docstring; it should not be wrapped in quotes.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.