18  Doctest: The Absolute Minimum You Must Know

A doctest is an example that is also a test: you paste an interpreter session into a docstring, and Python re-runs it to check the outputs still hold. The whole tool is one idea and three rules of transcript-writing — all on this page.

18.1 The Idea: Examples That Cannot Rot

Documentation lies. Not on the day it’s written — later, when the code changes and the examples don’t. Doctest kills that failure mode: because the example is a test, an example that stops being true becomes a build failure instead of a trap for the next reader. That’s the honest-documentation guarantee, and no comment or README paragraph can offer it.

A doctest lives in a docstring and looks exactly like the interactive interpreter:

# temperature.py
def fahrenheit(celsius):
    """Convert Celsius to Fahrenheit.

    >>> fahrenheit(100)
    212.0
    >>> fahrenheit(-40)
    -40.0
    """
    return celsius * 9 / 5 + 32

Lines starting >>> are statements to run (... continues a multi-line one); the lines below each are the expected output, matched exactly — same text, same spacing.

18.2 Writing a Transcript

The reliable way to write one is not to write it at all: run the code in a real interpreter and paste the session. Everything doctest-shaped in this page is live, so the rules below demonstrate themselves. First the function:

def fahrenheit(celsius):
    return celsius * 9 / 5 + 32

Rule 1 — expressions are compared against their repr. That’s why strings show their quotes, and why print output doesn’t:

>>> fahrenheit(-40)      # a float's repr: 212 would fail, -40.0 passes
-40.0
>>> "Ada"                # expression → repr → quotes included
'Ada'
>>> print("Ada")         # print → the text itself → no quotes
Ada
>>> total = 3 + 4        # assignment produces no output: expect none

Mixing these up is the classic doctest stumble — expecting Ada from a bare expression fails on the missing quotes.

Rule 2 — exceptions get a stub traceback. Write the header line, a literal ... for the messy middle, then the final error line:

>>> int("twelve")
Traceback (most recent call last):
  ...
ValueError: invalid literal for int() with base 10: 'twelve'

Rule 3 — a blank output line must be spelled <BLANKLINE>, because a real blank line means “the expected output ends here”:

>>> print("above\n\nbelow")
above
<BLANKLINE>
below

Keep transcripts deterministic: no random, no clock, no dict-of-object reprs like <Thing at 0x7f...> — if the output can vary, doctest is the wrong tool for that line.

18.3 Running Doctests

python -m doctest temperature.py      # silence means every example passed
python -m doctest -v temperature.py   # narrate each example as it runs
python -m doctest notes.md            # plain-text files with >>> in them work too
pytest --doctest-modules              # let pytest collect doctests with your tests

Silence-on-success surprises people: no news is a pass. Add -v when you want proof.

18.4 When Doctest Beats pytest — and When It Doesn’t

Doctest wins wherever a human will read the example anyway: docstrings of pure functions, tutorials, READMEs. One artefact serves as spec, documentation, and test, and it can’t drift. It loses everywhere else: no fixtures or parametrisation, clumsy with setup and side effects, brittle with floats and volatile reprs, and its failure output is thin. The working split: doctest for the contract you show humans, pytest for the deep coverage — a couple of honest examples per function, and the exhaustive edge-case grid in test_*.py.

18.5 The Meta-Point: This Page Is Testing Itself

This very repository eats its own cooking: scripts/test_docs.py executes every python block in these documents — blocks containing >>> run as doctests, the rest are exec’d into the same per-file namespace — and CI fails if any example is wrong. The fahrenheit transcripts above didn’t just look right, they ran. That’s the doctest idea scaled up to a whole book: it cannot lie about its own examples.

18.6 Directing the Machine

An AI writes docstring examples from the code’s apparent intent, not from running it — so the expected outputs are guesses, and a wrong guess becomes confident false documentation. The informed prompt names the transcript rules and demands the proof.

Vague:

"add doctests to slugify"

Informed:

"Add docstring examples to slugify covering a title with punctuation, internal
whitespace collapsing, and the empty string. Outputs must be exact reprs — quotes
included. Then run `python -m doctest slugify.py -v` and paste the run: every
example must pass, and I want to see the PASS lines, not a claim."

18.7 Spot the Confabulation

An AI assistant documents a helper “with tested examples”:

def average(nums):
    """Mean of a list of numbers.

    >>> average([1, 2, 3])
    2.0
    >>> average([])
    0
    """
    return sum(nums) / len(nums)
What’s wrong?

The second example documents behaviour the code doesn’t have: average([]) divides by len([]) and raises ZeroDivisionError — it does not return 0. The AI wrote the example it wished were true (graceful empty-list handling is what a helper “should” do), which is exactly the confabulation pattern: plausible, confident, unexecuted. And this is doctest’s whole value — python -m doctest fails immediately on that docstring, turning a lie in the documentation into a red build. Run the examples; never trust a transcript nobody executed.

18.8 Where to Practice

  • This repository — clone it, change one expected output in any doc, run python3 scripts/test_docs.py, and watch the book catch its own lie; then write a new passing block. No signup, and the feedback loop is seconds.
  • Your own codebase — pick one pure utility function, paste a real interpreter session into its docstring, and wire python -m doctest (or pytest --doctest-modules) into
    1. One function is enough to make the habit stick.

18.9 Quick Reference

Thing The minimum
>>> line a statement to run; ... continues it
Line(s) below expected output, matched exactly
Expression result compared against its repr — strings keep their quotes
print(...) compared against the printed text — no quotes
No output expected assignments, imports, None
Exception Traceback (most recent call last): + ... + final error line
<BLANKLINE> stands for an empty line in expected output
python -m doctest f.py run a file’s examples; silence = pass
python -m doctest -v f.py narrate every example
pytest --doctest-modules collect doctests alongside pytest tests
Use doctest for the honest examples humans read
Use pytest for setup, side effects, exhaustive edge cases

That covers the absolute minimum! You can now write documentation that proves itself on every run — and the moment an example outgrows a transcript, you know it belongs in pytest instead.