Expert · Lesson 84 of 94

Documenting code

Watch this lesson

View full playlist ↗

Read the lesson

What you'll learn

Concept

Code is read far more often than it is written, and most of that reading is by someone deciding whether to call a function, not how it works. A docstring answers that reader in one place: what the function does, what it takes, what it returns, what it raises, and one example. Python attaches it to the object, help() prints it, editors show it on hover, and documentation generators turn it into a site. A function without a docstring forces every caller to read its body.

Documenting code: docstring on the function → help(f) in the REPL → editor hover / autocomplete → doctest runs the examples → pdoc / mkdocs → HTML site
Documenting code · Figure 1 · Open for full size

Keep to one convention across a project. Google style, Args:, Returns:, Raises:, Example:, is the most readable in plain text and what this course uses; NumPy and Sphinx styles exist and tools understand all three. The first line is a one-sentence summary in the imperative, "Parse a line into an Expense", ending in a period, followed by a blank line and the details. Document what a caller needs, not the implementation: behavior, edge cases, units, and errors. Do not repeat the types, because the type hints already say them; say what the values mean.

Docstring part Answers Example line
summary line what does it do? Parse one CSV line into an Expense.
Args: what do the parameters mean, in what units? line: a CSV row with day, category, amount.
Returns: what comes back, and when it is None? The parsed Expense.
Raises: which errors, under what conditions? ParseError: if a field is missing or invalid.
Example: / >>> how is it called? >>> parse_amount("€12.50")

Examples rot unless something runs them. doctest scans docstrings for >>> lines, executes them, and compares the printed result with the text below; python -m doctest module.py or pytest --doctest-modules runs every example as a test. A docstring example that is also a test cannot lie. Keep examples short and deterministic, avoid anything with unstable output like dictionaries of floats or timestamps, and use # doctest: +ELLIPSIS when part of the output does not matter.

Type hints are documentation the tools verify, so a well-hinted signature with a good docstring is complete; pdoc renders both into an HTML site for the whole package with one command and no configuration, which is the right first documentation site for a library. MkDocs with the mkdocstrings plugin is the step up when you want prose pages alongside the API reference. Above all sits the README: what the project does in one paragraph, how to install it, one example, and where to go next. It is the page everyone reads and most of them stop at, so write it first and keep it current.

Documenting code: README: what, install, one example, links → API reference generated from docstrings + hints → guides: prose pages for tasks (MkDocs) → CHANGELOG: what changed per version
Documenting code · Figure 2 · Open for full size

Code

A module with complete docstrings in Google style, including runnable examples. help() shows them exactly as written.

# money.py
"""Money helpers: parsing amounts and splitting bills.

All amounts are in euros unless stated otherwise.
"""


def parse_amount(text: str) -> float:
    """Parse a money string like ``"€12.50"`` or ``"12,50"`` into euros.

    Accepts an optional leading euro sign and either ``.`` or ``,`` as the
    decimal separator. Whitespace around the value is ignored.

    Args:
        text: The raw amount as typed by a user or read from a file.

    Returns:
        The amount rounded to cents.

    Raises:
        ValueError: If ``text`` is empty, negative, or not a number.

    Example:
        >>> parse_amount("€12.50")
        12.5
        >>> parse_amount(" 7,25 ")
        7.25
    """
    cleaned = text.strip().removeprefix("€")
    if "," in cleaned and "." not in cleaned:
        cleaned = cleaned.replace(",", ".")
    if not cleaned:
        raise ValueError("empty amount")
    value = round(float(cleaned), 2)
    if value < 0:
        raise ValueError("amount must not be negative")
    return value


def split_evenly(total: float, people: int) -> list[float]:
    """Split ``total`` across ``people``, distributing leftover cents to the first shares.

    Args:
        total: The amount to split, in euros.
        people: How many shares; must be at least 1.

    Returns:
        One share per person, summing exactly to ``total``.

    Raises:
        ValueError: If ``people`` is less than 1.

    Example:
        >>> split_evenly(10.0, 3)
        [3.34, 3.33, 3.33]
    """
    if people < 1:
        raise ValueError("people must be at least 1")
    cents = round(total * 100)
    base, extra = divmod(cents, people)
    return [(base + (1 if i < extra else 0)) / 100 for i in range(people)]


if __name__ == "__main__":
    help(parse_amount)
Output
Help on function parse_amount in module __main__:

parse_amount(text: str) -> float
    Parse a money string like ``"€12.50"`` or ``"12,50"`` into euros.

    Accepts an optional leading euro sign and either ``.`` or ``,`` as the
    decimal separator. Whitespace around the value is ignored.

    Args:
        text: The raw amount as typed by a user or read from a file.

    Returns:
        The amount rounded to cents.

    Raises:
        ValueError: If ``text`` is empty, negative, or not a number.

    Example:
        >>> parse_amount("€12.50")
        12.5
        >>> parse_amount(" 7,25 ")
        7.25

Running the examples as tests. Both docstrings pass; then a deliberately wrong example is added to a copy to show what a failing doctest reports.

# run_doctest.sh
python -m doctest -v money.py | tail -3
sed 's/\[3.34, 3.33, 3.33\]/[3.33, 3.33, 3.34]/' money.py > money_wrong.py
python -m doctest money_wrong.py | grep -E "Expected|Got|^\s+\[" | head -4
Output
3 tests in 3 items.
3 passed.
Test passed.
Expected:
    [3.33, 3.33, 3.34]
Got:
    [3.34, 3.33, 3.33]

Docstrings and hints read back programmatically, which is what every documentation tool does: inspect gives the signature, __doc__ the text, and the annotations the types.

# introspect.py
import inspect

import money

for name, obj in inspect.getmembers(money, inspect.isfunction):
    sig = inspect.signature(obj)
    summary = inspect.getdoc(obj).splitlines()[0]
    print(f"{name}{sig}")
    print(f"    {summary}")
    print(f"    raises: {[ln.strip().split(':')[0] for ln in inspect.getdoc(obj).splitlines() if ln.strip().endswith(('.',)) and 'Error' in ln][0]}")
print(money.__doc__.splitlines()[0])
Output
parse_amount(text: str) -> float
    Parse a money string like ``"€12.50"`` or ``"12,50"`` into euros.
    raises: ValueError
split_evenly(total: float, people: int) -> list[float]
    Split ``total`` across ``people``, distributing leftover cents to the first shares.
    raises: ValueError
Money helpers: parsing amounts and splitting bills.

Generating a site with pdoc and writing the README that points to it. pdoc is installed only for this block; the README is the front door and needs only four things.

# site_and_readme.sh
uv run --with pdoc python -m pdoc money -o site >/dev/null 2>&1
ls site
grep -c "parse_amount" site/money.html | xargs -I{} echo "parse_amount mentioned {} times in the generated page"
cat > README.md <<'MD'
# money

Parse money strings and split bills without losing cents.

## Install

    pip install money-helpers

## Example

    >>> from money import parse_amount, split_evenly
    >>> split_evenly(parse_amount("€10"), 3)
    [3.34, 3.33, 3.33]

## More

API reference: https://example.com/money/ · Changelog: CHANGELOG.md · Issues: GitHub
MD
grep -E "^#|^    >>>" README.md
Output
index.html
money.html
search.js
parse_amount mentioned 42 times in the generated page
# money
## Install
## Example
    >>> from money import parse_amount, split_evenly
    >>> split_evenly(parse_amount("€10"), 3)
## More

Try it yourself

Add Google-style docstrings with a runnable example to every public function and class in the expense library, then run pytest --doctest-modules src and fix any example that fails. Generate a site with pdoc src/expenses -o site and open site/index.html. Write the project README with the four sections from this lesson, and check that a stranger could install and run one command from it without reading anything else.

Common mistakes

What's next

Your code explains itself. Next you will keep its settings and secrets out of the code entirely: configuration from the environment and TOML.

Course overview