PYTHON / FUNCTIONS
Docstrings and the contract of a function
Write docstrings that state a function's contract, and inspect them at runtime through __doc__, inspect.getdoc, and doctest.
What you will learn
- Place a string literal as the very first statement so it becomes __doc__
- State caller preconditions, the return guarantee, and exceptions that are raised
- Read docstrings with inspect.getdoc to get indentation stripped correctly
- Turn contract examples into doctests so the promise is checked by running them
Understanding Docstrings and the contract of a function
A docstring is not a comment. When Python compiles a function whose first statement is a plain string literal, it stores that string in the code object and binds it to the function's __doc__ attribute, so it exists as data at runtime. That is why help(), IDE tooltips, and doctest can all see it while a # comment on the same line is discarded by the tokenizer and can never be recovered.
The useful content of a docstring is the function's contract: what the caller must guarantee before calling, and what the function guarantees in return. The signature already shows parameter names and count, so repeating them adds nothing; what it cannot show is that low must not exceed high, that a ValueError is raised instead of a wrong answer, that the list argument is mutated in place, or that the return value is a new object rather than a view. Those are the facts a caller needs and cannot get from the signature.
Describing behaviour rather than implementation is what keeps a docstring true. The body may switch from a loop to a comprehension to a C library call without breaking anyone, but changing "returns None when the key is missing" to "raises KeyError" breaks every caller written against the old text. Writing the examples as doctests makes the strongest part of the contract executable, so the promise fails loudly when the code drifts away from it.
import inspect
def clamp(value, low, high):
"""Return value limited to the range [low, high].
The caller must pass low <= high; otherwise the range is empty
and ValueError is raised rather than a silently wrong answer.
"""
if low > high:
raise ValueError(f"empty range: low={low} > high={high}")
return min(max(value, low), high)
print(clamp(15, 0, 10))
print(repr(clamp.__doc__.splitlines()[2]))
print(repr(inspect.getdoc(clamp).splitlines()[2]))
try:
clamp(5, 10, 0)
except ValueError as exc:
print("ValueError:", exc)
A docstring is a runtime string object holding the function's contract: the caller's obligations and the function's guarantees, not a retelling of its code.
Worked examples
Only a leading literal becomes a docstring
Shows the three ways a string in a function body ends up as __doc__ or as nothing at all.
def with_doc():
# comments are not statements, so this one is harmless
"""Return the number one."""
return 1
def moved_doc():
x = 1
"""Not a docstring: an unused string expression after a statement."""
return x
def fstring_doc():
f"""Not a docstring: {'f-strings'} are not plain literals."""
return 2
for fn in (with_doc, moved_doc, fstring_doc):
print(fn.__name__, "->", repr(fn.__doc__))
Example explained
Line 1In with_doc the comment is stripped at tokenizing time, so the string is still the first statement and is captured.
Line 2In moved_doc the assignment runs first, so the string is evaluated and thrown away; __doc__ stays None.
Line 3An f-string compiles to a formatting operation, not a constant, so fstring_doc has no docstring either.
Line 4None means help() shows nothing and doctest finds no examples, with no error to warn you.
A contract you can execute
Uses doctest to check that the documented success case and the documented failure case both still hold.
import doctest
def parse_pair(text):
"""Split "key=value" into a (key, value) tuple.
Splits on the first '=' only, so values may contain '='.
>>> parse_pair("host=localhost")
('host', 'localhost')
>>> parse_pair("no-equals")
Traceback (most recent call last):
...
ValueError: expected 'key=value', got 'no-equals'
"""
if "=" not in text:
raise ValueError(f"expected 'key=value', got {text!r}")
key, value = text.split("=", 1)
return key, value
results = doctest.testmod()
print("failed:", results.failed, "attempted:", results.attempted)
Example explained
Line 1doctest.testmod() scans docstrings in the current module and runs every line beginning with >>>.
Line 2The first example pins the exact return shape, a tuple, not a list or a dict.
Line 3The traceback example makes the raised exception part of the tested contract; doctest ignores the stack body but compares the ValueError message exactly.
Line 4attempted is 2 because two >>> examples were found, and failed is 0 because both matched.
__doc__ is a writable attribute
Attaches a docstring to a dynamically built function, which is the only option when the text depends on runtime values.
def make_scaler(factor):
def scale(value):
return value * factor
scale.__doc__ = f"Multiply value by {factor}; returns a new number."
return scale
double = make_scaler(2)
print(double(21))
print(double.__doc__)
Example explained
Line 1The inner scale has no literal docstring, so __doc__ starts out as None.
Line 2Assigning to scale.__doc__ sets it afterwards, which is why an f-string works here but not as the first statement.
Line 3Each call to make_scaler produces a separate function object with its own docstring text.
Line 4Tools that read source files rather than imported objects will not see a docstring assigned this way.
Important notes
Running python -OO discards all docstrings, so __doc__ becomes None; never let program logic parse or depend on docstring text.
Do not hand-align continuation lines to column zero to look tidy; keep the body indented like normal code and let inspect.getdoc or help() remove the common margin.
Common mistakes
Putting the string after an import, assignment, or inside an if at the top of the body: __doc__ is None, help() prints only the signature, and doctest silently collects nothing.
Using an f-string so version numbers or defaults can be interpolated: the result is a runtime expression, not a docstring, so __doc__ is None with no error.
Narrating the code ("loops over the items and appends to a list") instead of the contract: readers still have to open the body to learn whether the input is mutated or what happens on bad input, and the text goes stale on the first refactor.
Try it yourself
Change, predict, then run
Write take(items, n) that returns the first n items, documenting in its docstring that n must be >= 0 and that ValueError is raised otherwise, with one doctest for a normal call and one for the ValueError. Run doctest.testmod() and confirm 0 failures, then print inspect.getdoc(take).
Open the Python workspaceCheck your understanding
A function's docstring says it returns None when a key is missing. A refactor makes it raise KeyError instead, and every caller inside the repository is updated. Why is leaving the docstring unchanged still a defect?
- Python compares the docstring with the body at import time and emits a SyntaxWarning on mismatch.
- The stale text makes the function's __doc__ evaluate to None until the module is recompiled.
- The docstring is the function's published contract, so anyone reading help() or writing new calls will code against a guarantee the function no longer honours.
- Nothing is wrong, because a docstring is a comment and cannot affect callers.
Show answer
The docstring is the only statement of the contract available to callers who are not reading the body, so a false promise there produces callers that test for None and never handle KeyError. Option 3 is tempting because nothing crashes immediately, but a docstring is not a comment: it survives compilation as __doc__ and is what help(), IDEs, and doctest report. Python performs no consistency checking, so options 0 and 1 describe behaviour that does not exist.