Documenting Your Code Clearly

Documentation is the message from your past self to your future self. Good docs answer three questions a future reader asks: what does this do?, what are the inputs and outputs?, and why does it work this way?. In Python the most common form is the docstring — a string literal at the top of a module, class, or function — and it is the single highest-leverage documentation habit you can adopt.

A docstring is a string that appears as the first statement of a module, class or function. Python captures it in the special attribute __doc__, and tools like help(), the REPL, IDE tooltips, and documentation generators (pdoc, sphinx) surface it automatically. One short docstring often replaces pages of external documentation.

Follow PEP 257 for structure. One-liner: a single sentence ending in a period. Multi-line: a summary line, a blank line, then longer detail. Include the expected types, behavior on edge cases, and any exceptions raised. Tools parse sectioned docstrings (Google-style, NumPy-style, reStructuredText); pick one style per codebase and stick with it.

Beyond docstrings, two forms of documentation deserve attention. Type hints (def fn(x: int) -> str:) are structured documentation your tools can check. A short README.md at the project root answers “what is this project and how do I run it?” in under a minute. Everything else is optional; these three cover most real needs.

Docstring styles

Google-style: sections like Args:, Returns:, Raises:. Compact, readable on any screen, well supported. NumPy-style: underlined section headers; popular in scientific projects. reStructuredText: field lists (:param x:); traditional but verbose.

For a library, also write module-level docstrings that describe what the whole module is for, and class docstrings that describe the role of each class. Put code examples in docstrings when they help; doctest can even run them as tests.

Types as documentation

A good type hint is self-documenting: def load_users(path: Path) -> list[User] communicates the contract at a glance. Tools like mypy enforce the contract; humans benefit regardless.

Type aliases make long signatures readable: UserId = NewType("UserId", int) distinguishes a user id from a plain int. Literal["a", "b"] documents accepted string values precisely.

Documentation tools and conventions.

ToolPurpose
PEP 257
style guide
Docstring conventions.
pydoc
tool
Built-in doc browser.
help(obj)
built-in
Show the docstring interactively.
doctest
module
Run examples in docstrings as tests.
pdoc
tool
Modern static doc generator.
Sphinx
tool
Long-form docs; used by the Python docs.
PEP 484
spec
Type hints.
PEP 526
spec
Variable annotations.

Documenting Your Code Clearly code example

The script defines a well-documented function, verifies its docstring with doctest, and prints help() output.

# Lesson: Documenting Your Code Clearly
"""Small library: stats helpers that are fully documented."""

from statistics import mean


def weighted_average(values: list[float], weights: list[float]) -> float:
    """Return the weighted arithmetic mean.

    Args:
        values: Numeric observations.
        weights: Non-negative weights for each observation. Must be the same
            length as ``values`` and sum to a positive number.

    Returns:
        The mean of ``values`` weighted by ``weights``.

    Raises:
        ValueError: If lengths differ or all weights are zero.

    Examples:
        >>> weighted_average([1, 2, 3], [1, 1, 1])
        2.0
        >>> weighted_average([10, 20], [3, 1])
        12.5
    """
    if len(values) != len(weights):
        raise ValueError("values and weights must be the same length")
    total_weight = sum(weights)
    if total_weight <= 0:
        raise ValueError("sum of weights must be positive")
    return sum(v * w for v, w in zip(values, weights)) / total_weight


print(weighted_average([1, 2, 3], [1, 1, 1]))
print(weighted_average([10, 20], [3, 1]))
print("docstring:", weighted_average.__doc__.splitlines()[0])

# Run the doctest examples embedded above
import doctest
results = doctest.testmod(verbose=False)
print("doctest attempted:", results.attempted, "| failed:", results.failed)

What makes this docstring pay off:

1) First line is a single-sentence summary; `help()` and IDE tooltips show it.
2) `Args:` / `Returns:` / `Raises:` sections keep the contract explicit.
3) `Examples:` double as runnable doctests — docs that cannot drift.
4) Type hints + docstring together say everything a caller needs.

Add a docstring to a small function.

def chunked(seq: list, n: int) -> list[list]:
    """Split *seq* into consecutive lists of up to *n* items.

    Args:
        seq: Input sequence.
        n: Chunk size. Must be > 0.

    Returns:
        A list of chunks. The last chunk may be shorter.

    Examples:
        >>> chunked([1, 2, 3, 4, 5], 2)
        [[1, 2], [3, 4], [5]]
    """
    if n <= 0:
        raise ValueError("n must be positive")
    return [seq[i:i+n] for i in range(0, len(seq), n)]

print(chunked.__doc__.splitlines()[0])

Cheap ways to check docstrings exist.

def f(x: int) -> int:
    """Double x."""
    return x * 2
assert f.__doc__ == "Double x."
assert f.__annotations__ == {"x": int, "return": int}

Running prints:

2.0
12.5
docstring: Return the weighted arithmetic mean.
doctest attempted: 2 | failed: 0