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.
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 |
|---|---|
| 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:
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
paidstate.” - Side effects. “Already persisted” - the caller need not save.
- Exceptions, and the condition that raises them.
- Units and bounds.
timeout: secondsprevents 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.
[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.
"""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.
Related
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
Drules withconvention = "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.