Docstrings and doctest

Intermediate40 min

Document functions with docstrings that help() and editors can show, and put runnable examples in them so the documentation is tested.

What you will learn
  • Write a docstring with a summary line, and describe arguments, return value and errors
  • Read docstrings back with __doc__, help() and inspect
  • Embed examples in a docstring and run them with doctest
  • Use ELLIPSIS, NORMALIZE_WHITESPACE and SKIP, and run doctests from a text file
On this page

Documentation that lives in a separate file goes out of date quietly. A docstring sits right inside the function it describes, where the person changing the code will see it. And if the docstring contains examples that Python can run, the documentation can never silently disagree with the code. The fundamentals lesson on reading docs covers how readers use documentation. This lesson covers writing it.

What a docstring is

A docstring is a string literal that is the first statement of a module, function, class or method. Python stores it in the __doc__ attribute. The comments and style lesson introduced the idea. Here is a function with a full docstring:

c37_docstring.py
import inspect
def resize(width, height, factor=1.0):
"""Scale a width and height by a factor.
Args:
width: Width in pixels.
height: Height in pixels.
factor: Multiplier, 1.0 leaves the size unchanged.
Returns:
A (width, height) tuple of ints.
Raises:
ValueError: If factor is not positive.
"""
if factor <= 0:
raise ValueError("factor must be positive")
return round(width * factor), round(height * factor)

The docstring is stored on the function, and the standard inspect module can show it:

c37_docstring.py (continued)
print(resize.__doc__.splitlines()[0])
print("---")
print(inspect.getdoc(resize))
print("---")
print(inspect.signature(resize))
Output
Scale a width and height by a factor.
---
Scale a width and height by a factor.
Args:
width: Width in pixels.
height: Height in pixels.
factor: Multiplier, 1.0 leaves the size unchanged.
Returns:
A (width, height) tuple of ints.
Raises:
ValueError: If factor is not positive.
---
(width, height, factor=1.0)

inspect.getdoc returns the docstring with the indentation cleaned up, which is what help(resize) prints in the REPL, next to the signature that inspect.signature shows. Editors use the same text for hover tips, so a good docstring helps everyone who calls the function without reading its body.

The conventions are short:

  • Triple double quotes, even for one line.
  • A summary line first: one sentence, in the imperative (“Scale…”, not “Scales…”), ending with a full stop.
  • If more is needed, a blank line and then the details.
  • Describe what the function promises, not how it works inside. The code already says how.

PEP 257 is the standard reference. For the argument sections, projects pick one layout and stay with it. The Args: / Returns: / Raises: style above (called Google style) is common, and NumPy style, with underlined section titles, is another. Use whatever the project already uses. Type information belongs in the annotations from type hints, not repeated in the text.

Examples that run: doctest

A docstring can contain something that looks like an interactive session. Lines starting with >>> are code, and the lines under them are the expected output. The standard doctest module finds those and runs them. We build a small statistics module in three steps, one function each. First mean, whose docstring shows two results and an error:

c37_mean.py
def mean(values):
"""Return the arithmetic mean of a non-empty sequence of numbers.
>>> mean([2, 4, 6])
4.0
>>> mean([5])
5.0
An empty sequence is an error:
>>> mean([])
Traceback (most recent call last):
...
ValueError: mean() needs at least one value
"""
if not values:
raise ValueError("mean() needs at least one value")
return sum(values) / len(values)
if __name__ == "__main__":
import doctest
print(doctest.testmod())
Output
TestResults(failed=0, attempted=3)

doctest.testmod() runs the examples in every docstring of the module it is called from and returns a result with the number of examples attempted and failed. Silence means success, apart from that summary. Second, clamp, with three examples and the same runner:

c37_clamp.py
def clamp(value, low, high):
"""Limit value to the range low..high.
>>> clamp(15, 0, 10)
10
>>> clamp(-3, 0, 10)
0
>>> clamp(7, 0, 10)
7
"""
return max(low, min(value, high))
if __name__ == "__main__":
import doctest
print(doctest.testmod())
Output
TestResults(failed=0, attempted=3)

Third, histogram, whose docstring has a sentence of prose between the description and the example:

c37_histogram.py
def histogram(words):
"""Count how often each word occurs, most common first.
The result is a list of (word, count) pairs:
>>> histogram(["b", "a", "b", "c", "b", "a"])
[('b', 3), ('a', 2), ('c', 1)]
"""
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
return sorted(counts.items(), key=lambda pair: -pair[1])
if __name__ == "__main__":
import doctest
print(doctest.testmod())
Output
TestResults(failed=0, attempted=1)

You can also run doctests without editing the file, and add -v to see each example:

Terminal window
$ python -m doctest c37_mean.py
$ python -m doctest -v c37_mean.py

The first prints nothing when all examples pass. The second lists every example and ends with 3 passed. Exceptions work too. The expected output starts with Traceback (most recent call last):, then an ellipsis line (the middle of the traceback is ignored), and then the exception line, which must match.

When an example fails

Here is a docstring with a wrong expectation. Ten with 20 per cent tax is 12.0, not 12.5:

c37_doctest_fail.py
def add_tax(price, rate):
"""Return price plus tax.
>>> add_tax(10, 0.2)
12.5
>>> add_tax(100, 0.2)
120.0
"""
return round(price * (1 + rate), 2)

Running it prints the report below. Your report shows the full path of the file instead of just its name.

Terminal window
$ python -m doctest c37_doctest_fail.py
**********************************************************************
File "c37_doctest_fail.py", line 4, in c37_doctest_fail.add_tax
Failed example:
add_tax(10, 0.2)
Expected:
12.5
Got:
12.0
**********************************************************************
1 item had failures:
1 of 2 in c37_doctest_fail.add_tax
***Test Failed*** 1 failure.
$ echo $?
1

The report names the file, the line of the example and the function, and then puts Expected above Got. The exit status is 1, so doctest is suitable for the one-command check from the test-driven workflow lesson. The second example passed, and so only one failure is reported.

Making examples robust

doctest compares output character by character, which is strict. Three option flags relax it. Write them in a comment on the example line.

c37_flags.py
import doctest
def describe(obj):
"""Show an object the way a debugger would.
The address changes on every run, so ELLIPSIS matches it loosely:
>>> describe(object()) # doctest: +ELLIPSIS
'<object object at 0x...>'
Long output can be laid out freely when whitespace is normalised:
>>> list(range(12)) # doctest: +NORMALIZE_WHITESPACE
[0, 1, 2, 3,
4, 5, 6, 7,
8, 9, 10, 11]
A line that is slow or needs a network can be skipped:
>>> describe(open("/no/such/file")) # doctest: +SKIP
"""
return repr(obj)

The runner at the bottom counts the examples:

c37_flags.py (continued)
result = doctest.testmod()
print(result.failed, "failed,", result.attempted, "attempted")
Output
0 failed, 3 attempted

+ELLIPSIS lets ... stand for any text, useful for memory addresses, timestamps and long tails. +NORMALIZE_WHITESPACE treats any run of whitespace as equal, so you can wrap long output. +SKIP leaves an example in the documentation without running it. If you find yourself using many flags, the example is probably too clever for documentation. Keep doctests for short, stable examples, and use real tests for the rest.

Other things to watch: dictionaries print in insertion order, so {'a': 1, 'b': 2} is reliable as output, but sets print in an order you should not depend on. Wrap them in sorted(...). Floats print their shortest representation, so 0.1 + 0.2 shows 0.30000000000000004, and round(...) gives you stable text.

Doctests in a plain text file

Examples can also live in a text guide, with prose around them. This helps for a README that shows how to use a package:

c37_guide.txt
Using the stats helpers
=======================
This file is a plain text guide. Every line that starts with ">>> " is a test.
>>> from c37_mean import mean
>>> from c37_clamp import clamp
>>> mean([1, 2, 3, 4])
2.5
The same names stay available further down the file:
>>> clamp(mean([10, 20]), 0, 12)
12
c37_testfile.py
import doctest
print(doctest.testfile("c37_guide.txt"))
Output
TestResults(failed=0, attempted=4)

The whole file shares one namespace, which is why mean and clamp, imported in the first examples, are available in the later ones. If you use pytest, python -m pytest --doctest-modules collects the docstring examples of your modules, and --doctest-glob="*.txt" does the same for text files, so they run alongside your other tests.

Docstring or comment?

A docstring explains what and how to use, for a caller. A comment explains why, for the next person editing the code. A docstring is the right place for examples and for the contract: what the arguments mean and what is raised. A # comment is right for a surprising choice inside a function body. The README, then, is the first stop for a stranger, and the docstrings answer questions about a specific call.

Common pitfalls

  • Output that includes a changing value. Addresses, dates and random numbers make examples flaky. Use +ELLIPSIS, or restructure the example to print something stable.
  • A missing blank line after the expected output. Expected output ends at the next blank line or >>>. Text after the output without a blank line becomes part of it.
  • Backslashes in a docstring. "\n" in a normal docstring is an actual newline when the docstring is read. Use a raw docstring (r"""...""") if the examples contain backslashes.
  • print versus return value. >>> f() shows the repr of the return value, >>> print(f()) shows the printed text, so a string result appears with quotes in the first and without in the second.
  • Docstrings that restate the code. “Adds a and b” over def add(a, b) helps nobody. Say what is not obvious: units, ranges, what happens on bad input.
  • Examples that rot. A doctest only protects you if it runs. Add it to the one-command check.
ExerciseAdd doctests to a function

Write the function percent(part, whole) that returns part as a percentage of whole, rounded to one decimal place. Give it a docstring with four doctest examples: percent(1, 4) gives 25.0, percent(1, 3) gives 33.3, percent(0, 5) gives 0.0, and percent(1, 0) raises ValueError with the message whole must not be zero. Run the file: it should print TestResults(failed=0, attempted=4).

Show solution
c37_ex_percent.py
def percent(part, whole):
"""Return part as a percentage of whole, rounded to one decimal place.
>>> percent(1, 4)
25.0
>>> percent(1, 3)
33.3
>>> percent(0, 5)
0.0
A whole of zero has no percentage:
>>> percent(1, 0)
Traceback (most recent call last):
...
ValueError: whole must not be zero
"""
if whole == 0:
raise ValueError("whole must not be zero")
return round(part / whole * 100, 1)
if __name__ == "__main__":
import doctest
print(doctest.testmod())
Output
TestResults(failed=0, attempted=4)

The prose line before the last example is allowed: any text between examples is ignored. The error example needs only the exception type and message after the ... line.

ExerciseDocument and test a text-wrapping function

Write wrap_words(text, width), which splits text into a list of lines no longer than width, breaking only between words. A word longer than width gets its own line, an empty text gives [], and a width below 1 raises ValueError("width must be at least 1"). Write a module docstring and a Google-style function docstring with Args and Raises. Add doctest examples for ("the quick brown fox jumps", 10), the long-word case, the empty case and the error. Run the file: it should print TestResults(failed=0, attempted=4).

Show solution

Start with the docstring, which doubles as the specification. Without a body the function returns None, so the examples fail until you write one:

c37_ex_wrap.py
"""Break text into lines of a fixed width."""
def wrap_words(text, width):
"""Split text into lines of at most width characters.
Args:
text: The text to wrap.
width: Maximum line length. A longer word gets its own line.
Raises:
ValueError: If width is less than 1.
>>> wrap_words("the quick brown fox jumps", 10)
['the quick', 'brown fox', 'jumps']
>>> wrap_words("extraordinarily short", 5)
['extraordinarily', 'short']
>>> wrap_words("", 10)
[]
>>> wrap_words("x", 0)
Traceback (most recent call last):
...
ValueError: width must be at least 1
"""

Then the body, and the runner:

c37_ex_wrap.py (continued)
if width < 1:
raise ValueError("width must be at least 1")
lines = []
current = ""
for word in text.split():
if current and len(current) + 1 + len(word) > width:
lines.append(current)
current = word
else:
current = f"{current} {word}" if current else word
if current:
lines.append(current)
return lines
if __name__ == "__main__":
import doctest
print(doctest.testmod())
Output
TestResults(failed=0, attempted=4)
ExerciseMake the doctest pass

This doctest fails, and the function is correct. Change only the docstring so that the file prints TestResults(failed=0, attempted=1) and no report.

c37_ex_shout.py
def shout(text):
"""Return text in capitals with an exclamation mark.
>>> shout("hi")
HI!
"""
return text.upper() + "!"
if __name__ == "__main__":
import doctest
print(doctest.testmod())

Running it prints:

**********************************************************************
File "c37_ex_shout.py", line 4, in __main__.shout
Failed example:
shout("hi")
Expected:
HI!
Got:
'HI!'
**********************************************************************
1 item had failures:
1 of 1 in __main__.shout
***Test Failed*** 1 failure.
TestResults(failed=1, attempted=1)
Show solution

Expected HI! but got 'HI!'. In an interactive session, a string result is shown with its quotes, because the value’s repr is printed, and doctest compares text exactly. Put the quotes in the expected output:

c37_ex_shout_fixed.py
def shout(text):
"""Return text in capitals with an exclamation mark.
>>> shout("hi")
'HI!'
"""
return text.upper() + "!"
if __name__ == "__main__":
import doctest
print(doctest.testmod())
Output
TestResults(failed=0, attempted=1)

If you wanted the text without quotes, the example would have to call print(shout("hi")).

Summary

  • A docstring is the first string in a module, function or class. Give it a summary line in the imperative, then arguments, return value and errors in one consistent style. help(), inspect.getdoc and editors read it.
  • Lines starting with >>> in a docstring are doctests. Run them with doctest.testmod() or python -m doctest, and use +ELLIPSIS, +NORMALIZE_WHITESPACE and +SKIP for output that varies.
  • doctest.testfile and pytest’s --doctest-modules run examples from text files and modules alongside the rest of your tests.
  • The next lesson covers what your program says about itself while it runs.