Backend / Code quality / 07_docstrings_and_documentation.md

Docstrings and documentation

Updated 5 interview angles 4 min read source
On this page7
  1. The three styles
  2. What not to write
  3. Enforcement
  4. Docstrings as machine input
  5. Beyond docstrings
  6. Related
  7. Interview angle

Docstrings and documentation

Job descriptions that say “documentation (Google-style docstrings)” are asking whether you write for the next reader. The conventions are easy; knowing what belongs in a docstring and what does not is the actual skill.

The three styles

All three are conventions, not language features - the interpreter only stores the string. Tooling is what makes the choice matter.

python
def charge(order_id: str, amount: Decimal) -> Receipt:
    """Charge a paid order and return the receipt.

    Args:
        order_id: Must be in `paid` state.
        amount: Positive; currency from the order.

    Returns:
        The receipt, already persisted.

    Raises:
        AlreadyCharged: If a receipt already exists.
    """

That is Google style - indented sections, readable as plain text. It is the most common in application code and the easiest to scan in a terminal.

NumPy style uses underlined headings and is dominant in scientific libraries, where parameter tables get long. reST style uses :param x: field lists, is Sphinx’s native format, and is the least pleasant to read unrendered.

Style Reach for it when
Google Application code, readability first
NumPy Scientific code, long parameter lists
reST An existing Sphinx codebase uses it

Pick one and enforce it. Mixed styles in one codebase are worse than any single choice, because tooling parses one grammar and readers learn one shape.

What not to write

The failure is a docstring that restates the signature:

python
def charge(order_id: str, amount: Decimal) -> Receipt:
    """Charges the order.

    Args:
        order_id (str): The order id.   # says nothing
        amount (Decimal): The amount.   # says nothing
    """

Every word there is already in the signature, and repeating the type in the docstring is actively harmful - it is a second source of truth that goes stale when the annotation changes. With type hints present, omit types from the docstring text.

Document what the signature cannot express:

  • Preconditions. “Must be in paid state.”
  • Side effects. “Already persisted” - the caller need not save.
  • Exceptions, and the condition that raises them.
  • Units and bounds. timeout: seconds prevents a real bug.
  • Why, where the reason is not obvious from the code.

A one-line docstring is fine for an obvious function. An absent docstring on a public interface is not, and neither is a long one that says nothing.

Enforcement

Ruff implements pydocstyle as the D rules, so this is a config change rather than a new tool - see Linters and static analysis.

toml
[tool.ruff.lint]
extend-select = ["D"]

[tool.ruff.lint.pydocstyle]
convention = "google"

Selecting convention disables the rules that contradict it, which is what stops the linter demanding two incompatible shapes at once. Enforce presence on public API only - requiring a docstring on every private helper produces noise that people learn to skim past.

--fix cannot write a docstring for you. It handles formatting rules; substance is yours.

Docstrings as machine input

Two places where a docstring stops being documentation and becomes behaviour, which is worth knowing because both fail silently:

Doctests. Examples in a docstring can execute as tests, keeping them honest

  • an example that drifts from the code fails the suite.
python
    """Return the slug.

    >>> slugify("Hello World")
    'hello-world'
    """

Tool descriptions. When a function is exposed to a model as a tool, the docstring is the prompt the model reads to decide whether and how to call it. A vague docstring produces wrong tool calls, and no error appears - see Model Context Protocol — fundamentals.

That inverts the usual priority: in an agent codebase, the docstring on a tool function is production behaviour, not a comment. The Anthropic SDK’s tool runner goes further and generates the tool’s JSON schema from a Google-style Args: section, so the docstring is the contract the model sees - The Claude Messages API in practice.

Beyond docstrings

Docstrings answer “how do I call this”. They do not answer “why is it built this way”, which is where the expensive knowledge lives. That belongs in an architecture decision record or a module-level docstring explaining the design, not spread across function docstrings.

The related discipline is data profiling before you write the code that consumes a dataset: check null rates, cardinality, ranges and encoding on real data rather than trusting a schema. df.describe() and value_counts() are enough to catch the assumptions that break a pipeline in production - see Data profiling and validation.

Interview angle 5

  • “Which docstring style do you use?” - Google style for application code because it reads well unrendered; NumPy where parameter lists are long, reST if the codebase is already Sphinx. The important part is picking one and enforcing it, since mixed styles defeat both tooling and readers.
  • “What goes in a docstring?” - what the signature cannot say: preconditions, side effects, exceptions and their triggers, units and bounds, and the reasoning where it is not obvious. Not the types, which are already in the annotations and become a second source of truth that goes stale.
  • “How do you enforce it?” - Ruff’s D rules with convention = "google", which disables the contradicting rules. Enforce presence on public API only; requiring it everywhere generates noise people learn to ignore.
  • “When is a docstring not documentation?” - when it is a doctest, which executes and fails if the example drifts, and when the function is exposed to a model as a tool, where the docstring is the prompt. A vague tool docstring causes wrong calls with no error raised.
  • “What is data profiling and why before coding?” - checking null rates, cardinality, ranges and encoding against real data rather than trusting the schema. It catches the assumptions that otherwise fail in production, and it takes minutes.