Python Module 12 – Testing, debugging and code quality
doctest: examples that test themselves
Write doctests in docstrings, run them with python -m doctest and testmod(), and keep them passing when output changes, as with sets and object reprs.
What you will learn
- Write doctests, including an expected exception, inside docstrings
- Run doctests from the command line and from code, and read a failure report
- Fix doctests whose output varies between runs, such as sets and memory addresses
- Run doctests together with unit tests and check the examples of a README
- Decide when a check belongs in a doctest and when in a unit test
Before you start
What you’ll learn · Before you start
What you will learn
- Write doctests, including an expected exception, inside docstrings
- Run doctests from the command line and from code, and read a failure report
- Fix doctests whose output varies between runs, such as sets and memory addresses
- Run doctests together with unit tests and check the examples of a README
- Decide when a check belongs in a doctest and when in a unit test
Before you start
On this page
A docstring: A string written as the first statement of a module, class or function. Python keeps it as the object's __doc__ attribute, which help(), editors and doctest read. More in “Docstrings, comments and PEP 8 style” promises that inr(1234567) gives '₹12,34,567'; a later change to the code makes it give
'₹1,234,567', and the documentation goes on promising the old answer. doctest, a module of Python’s standard
library, catches that drift: it searches docstrings for text that looks like a session in the interactive shell, a
line starting with >>> followed by what the shell printed, runs each example and checks that it prints exactly
that text.
Your examples then do two jobs: they show readers how to call the function, and they fail as soon as the code stops doing what the documentation says. doctest suits small, pure functions and the examples of a module’s documentation; checks that need setup, many cases or large data belong in unittest or pytest, the next lessons of this module. Below, doctest checks five examples step by step, catches a stale one and fails two right functions on their text.
A doctest is a shell example and the text it should print
A function that splits a restaurant bill in paise, with five examples in its docstring and, at the bottom, a call that runs them and reports each one:
"""Split a restaurant bill in paise, so that the shares always add up to the total."""
def split_bill(total_paise, people):
"""Return each person's share in paise; the first shares carry the paise left over.
>>> split_bill(900_00, 3)
[30000, 30000, 30000]
>>> split_bill(100_00, 3)
[3334, 3333, 3333]
>>> sum(split_bill(100_00, 3))
10000
>>> split_bill(1, 4)
[1, 0, 0, 0]
>>> split_bill(500, 0)
Traceback (most recent call last):
...
ValueError: people must be at least 1, not 0
"""
if people < 1:
raise ValueError(f"people must be at least 1, not {people}")
share, left_over = divmod(total_paise, people)
return [share + 1 if i < left_over else share for i in range(people)]
if __name__ == "__main__":
import doctest
doctest.run_docstring_examples(split_bill, globals(), verbose=True, name="split_bill") Output
Finding tests in split_bill
Trying:
split_bill(900_00, 3)
Expecting:
[30000, 30000, 30000]
ok
Trying:
split_bill(100_00, 3)
Expecting:
[3334, 3333, 3333]
ok
Trying:
sum(split_bill(100_00, 3))
Expecting:
10000
ok
Trying:
split_bill(1, 4)
Expecting:
[1, 0, 0, 0]
ok
Trying:
split_bill(500, 0)
Expecting:
Traceback (most recent call last):
...
ValueError: people must be at least 1, not 0
ok
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bill.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Each example is a >>> line, plus ... lines when a statement continues, followed by the expected output, which
runs until a blank line or the next >>>. The expected output is what the interactive shell would show: the
repr() of an expression’s value, or whatever the code prints. An expected exception starts with the line
Traceback (most recent call last):; doctest ignores the stack after it, usually written as ..., and compares the
last line, the exception’s type and message.
Here is what doctest does with each example:
How doctest checks one example
Text description of the diagram
The diagram is a flow of five steps from top to bottom, ending in two outcomes.
- The docstring holds an example: the line >>> split_bill(100_00, 3) followed by the expected output [3334, 3333, 3333].
- doctest finds each example: a line starting with >>>, any continuation lines starting with ..., and the expected output, which runs up to the next blank line or the next >>>.
- It runs the code in a copy of the module's global names, so examples see the module's functions but do not change them.
- It captures what the interactive shell would print: anything passed to print(), or the repr of the value of an expression.
- It compares that text with the expected lines, character for character, unless option flags relax the comparison.
If the texts are the same, the example is ok. If they differ, doctest reports a failed example with the expected and the actual output.
Step through the same loop on the five examples of split_bill, as the run above printed them:
The drawing shows the whole scene; its 9 steps are listed below.
Every step as text
- Five examples. run_docstring_examples() reads the docstring of split_bill and finds five examples, four that should give a value and one that should raise.
- The first example. split_bill(900_00, 3) is the first >>> line. Its expected output is the line below it, [30000, 30000, 30000], which ends where the next >>> line starts.
- Run it. doctest runs the call in a copy of the module's global names, so the example can use split_bill without changing anything the module holds.
- What the shell would show. The call returns a list, and the interactive shell would show its repr, so that is the text doctest keeps.
- Same text, ok. The text it got matches the expected line character for character, so the example passes and the verbose report prints ok.
- The paise left over. The loop takes the next example. 100_00 paise for 3 people gives [3334, 3333, 3333]: the first share carries the paisa left over, and the text matches.
- Two more values. sum(split_bill(100_00, 3)) prints 10000, the promise that the shares add up to the total, and split_bill(1, 4) prints [1, 0, 0, 0]. Both match.
- An expected exception. The last example, split_bill(500, 0), expects Traceback (most recent call last): as its first line, so doctest expects an exception instead of a value.
- Only the last line counts. The call raises ValueError. doctest skips the stack, written as ..., and compares the last line, ValueError: people must be at least 1, not 0. It matches, and all five pass.
Checkpoint
Check: An example and its expected text
doctest is silent until an example disagrees
There are three usual ways to run doctests:
python -m doctest bill.pyruns every example in the file and prints nothing when all of them pass; add-vto list each one,-o ELLIPSISto turn on an option and-fto stop at the first failure.doctest.testmod(), called at the bottom of a module, checks all of the module’s docstrings and returnsTestResults(failed, attempted); since Python 3.13 the result also counts skipped examples. The example above usesdoctest.run_docstring_examples(), which checks the docstring of one object.doctest.testfile("README.txt")checks the examples in a text file, such as a project’s documentation.
The value shows on the day the code and its documentation disagree. Here a function’s output was changed and its docstring was not:
cli/inr.py
def inr(amount):
"""Format whole rupees with Indian digit grouping.
>>> inr(1234567)
'₹12,34,567'
>>> inr(999)
'₹999'
"""
return f"₹{amount:,}" # a later "simplification": the , format groups digits in threes cli/run_doctest_cli.py
"""Runs python -m doctest on inr.py, as you would in a terminal, and shows its exit status."""
import subprocess
import sys
result = subprocess.run([sys.executable, "-m", "doctest", "inr.py"], capture_output=True, text=True)
print(result.stdout, end="")
print("exit status:", result.returncode) Output
**********************************************************************
File "inr.py", line 4, in inr.inr
Failed example:
inr(1234567)
Expected:
'₹12,34,567'
Got:
'₹1,234,567'
**********************************************************************
1 item had failures:
1 of 2 in inr.inr
***Test Failed*** 1 failure.
exit status: 1
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 run_doctest_cli.py
The report names the file and line, the example, the text it expected and the text it got. The second example of the docstring still passed, and the summary counts one failure out of two. The exit status of 1 matters as much: it makes a build or a continuous integration job stop, which a page of documentation could never do. In a terminal, Python 3.13 and later print the report in colour.
Key idea
doctest says nothing while the examples and the code agree; when they disagree it shows Expected and Got, and
python -m doctest exits with status 1, which stops a build.
Checkpoint
Check: Running doctests and reading a failure
Any difference in the text fails an example, even when the code is right
doctest compares text, character by character, so anything that prints differently from the docstring breaks an example even when the code is right. Here are two such examples, each as an author might first write it and then rewritten. For each failure the program prints the text doctest expected and the text it got:
"""Two doctests that fail although the code is right, and the same examples written so that they pass."""
import doctest
def average_bill(totals):
"""Return the average of some bills, in rupees.
>>> average_bill([250.1, 250.2, 250.3])
250.2
"""
return sum(totals) / len(totals)
def receipt(lines):
"""Print each item and its price, a blank line, then the total.
>>> receipt([("dosa", 60), ("chai", 20)])
dosa 60
chai 20
total 80
"""
for item, price in lines:
print(item, price)
print()
print("total", sum(price for _, price in lines))
def average_bill_rewritten(totals):
"""The same function, with the result rounded in the example.
>>> round(average_bill_rewritten([250.1, 250.2, 250.3]), 2)
250.2
"""
return sum(totals) / len(totals)
def receipt_rewritten(lines):
"""The same function, with the blank line written as <BLANKLINE>.
>>> receipt_rewritten([("dosa", 60), ("chai", 20)])
dosa 60
chai 20
<BLANKLINE>
total 80
"""
receipt(lines)
def check(function):
"""Runs the examples of one docstring and prints, for a failure, the text doctest expected and the text it got."""
[test] = doctest.DocTestFinder().find(function, function.__name__, globs=globals())
runner = doctest.DocTestRunner()
failures = []
runner.report_failure = lambda out, test, example, got: failures.append((example.want, got))
runner.run(test, out=lambda report: None)
print(f"{function.__name__}: {'FAILED' if failures else 'ok'}")
for want, got in failures:
print(f" expected {want!r}")
print(f" got {got!r}")
for function in (average_bill, receipt, average_bill_rewritten, receipt_rewritten):
check(function) Output
average_bill: FAILED
expected '250.2\n'
got '250.20000000000002\n'
receipt: FAILED
expected 'dosa 60\nchai 20\n'
got 'dosa 60\nchai 20\n\ntotal 80\n'
average_bill_rewritten: ok
receipt_rewritten: ok
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brittle.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The average is right, but the float prints as 250.20000000000002, not the 250.2 a person writes down, and
rounding inside the example fixes it. The receipt is right too, but its blank line ended the expected output, so
doctest expected two lines and got four; <BLANKLINE> stands for an empty line without ending it. The common
causes of such failures, each with its usual fix:
- Sets print their items in an order that can change between runs of Python; sort them in the example.
- Default reprs such as
<Order object at 0x10e2f3a50>contain a memory address that changes every run. - Long output is tiresome to copy in full and easy to get wrong by one character.
- Exception details can change: the module name in front of an exception class, or the wording of a message.
Three option flags cover most of the rest. ELLIPSIS lets ... in the expected output stand for any text.
NORMALIZE_WHITESPACE treats every run of spaces and line breaks as one space. IGNORE_EXCEPTION_DETAIL compares
only the exception’s type, without its module name or its message. A flag is turned on for one example with a
directive comment after its code, such as # doctest: +ELLIPSIS, or for a whole run with -o on the command line
or optionflags= in code.
Checkpoint
Check: Text that breaks an example
Check yourself
Practice that adapts to you
Practise this lesson
10 questions about 9 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Output that changes between runs, made stable Pro: opens with a Pro, Premium or Ultimate pass
A doctest that passes on one run and fails on the next is worse than none, because people learn to ignore it. This part reproduces the three commonest causes in programs that run, measures one of them, and fixes each for good.
In this part:
- Set order under 20 hash seeds, measured
- Memory addresses and long output, matched with two option flags
- Exception names that change with how a module is run
1 diagram · 3 runnable examples (Python, coupons.py, coupons_fixed.py, names.py, seeds.py, tags.py, tags_sorted.py) · about 8 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
doctest in a real project: suites, README files and limits Pro: opens with a Pro, Premium or Ultimate pass
doctest pays off most when it runs with the rest of a project’s tests and checks the examples people actually copy. This part adds doctests to a unittest run, checks a README file, and sets out where a doctest should give way to a unit test.
In this part:
- Doctests and unit tests in one run, with one exit status
- Examples in a README, checked with testfile()
- When a doctest should become a unit test, as a trade-off table
- Exercise: doctests that catch five bugs
- Doctests for a temperature converter
1 diagram · 2 runnable examples (README.txt, check_readme.py, discounts.py, test_discounts.py) · 1 exercise with sample tests · about 9 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
-
Fresher · Warm-up
What does doctest check, and how do you run the doctests of a module?
doctest, a module of Python's standard library, looks through docstrings for examples written like a session in the interactive shell: a line starting with >>> that holds code, followed by the output the shell would print. It runs each example and compares the text it actually prints with the expected text, character for character; an expected exception is written as the Traceback header line followed by the exception's last line. You run a module's doctests with python -m doctest module.py, which prints nothing when every example passes and a report with Expected and Got when one fails, or by calling doctest.testmod() at the bottom of the module. Add -v to see every example; doctest.testfile() checks the examples in a text file such as a README. The examples document the code and test it at the same time.
-
Fresher to mid
A doctest of a function that returns a set of usernames passes on your laptop and fails in CI about half the time, with the same Python version. What is happening, and how do you fix both the example and the habit behind it?
-
Mid to senior
A teammate proposes replacing the project's unit tests with doctests "because they double as documentation". Where would you agree, and where would you push back?
-
Mid to senior
A doctest that expects InvalidCoupon passes when you run python coupons.py but fails under python -m doctest coupons.py and under pytest --doctest-modules. Why, and what are two ways to write the example so that it passes everywhere?
Key takeaways
- doctest runs the
>>>examples of docstrings and compares what they print, as text, with the lines that follow. - An expected exception is the
Tracebackheader line, an optional...and the exception’s last line. python -m doctest file.py,doctest.testmod()anddoctest.testfile()run doctests, and a failure from the command line gives exit status 1.- Any difference in the text fails an example, so round floats, write
<BLANKLINE>, sort sets and useELLIPSIS,NORMALIZE_WHITESPACEandIGNORE_EXCEPTION_DETAILwhere they fit.
References
- doctest: test interactive Python examples (Python Software Foundation)
- doctest: option flags (Python Software Foundation)
- doctest: what about exceptions? (Python Software Foundation)
- doctest: unittest API (DocTestSuite) (Python Software Foundation)
- What's New in Python 3.13: doctest (Python Software Foundation)
- Command line and environment: PYTHONHASHSEED (Python Software Foundation)
- unittest: the load_tests protocol (Python Software Foundation)
- How to run doctests (pytest) (pytest-dev)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress