Docstrings are part of the public API. So are type hints. So are examples. Treat them like code.
def shard_for(user_id: str, shard_count: int) -> int:
"""Pick a shard for a user by stable hashing.
Routing must be deterministic: the same user lands on the same shard
across processes and restarts, so we hash the id rather than use
``hash()`` (which is salted per process). NOT cryptographically
secure — use ``hashlib`` for security boundaries, not routing.
Args:
user_id: Opaque account id. Must be non-empty.
shard_count: Number of shards. Must be positive.
Returns:
A shard index in ``range(shard_count)``.
Raises:
ValueError: If ``shard_count`` is not positive.
Example:
>>> shard_for("u_42", 4) in range(4)
True
"""
if shard_count <= 0:
raise ValueError(f"shard_count must be positive, got {shard_count}")
digest = hashlib.blake2b(user_id.encode(), digest_size=8).digest()
return int.from_bytes(digest) % shard_countThe summary is a one-line imperative that ends in a period, with a blank line before the details (14.4); the body explains why the id is hashed instead of hash() rather than restating what the code does (14.3). Args adds the units and constraints the type hints cannot — non-empty, positive — without echoing the types themselves (14.7); Returns and Raises document the contract (14.6); the runnable Example is doctest-checkable (14.5).
Reasoning, step by step:
- A docstring is the first thing
help(thing), the IDE tooltip, and documentation generators show. Without one, the contract lives in source-reading. - Public = anything that could be imported by another module (and isn't underscored or absent from
__all__). - Private functions and methods: docstring only when the name doesn't fully document.
- Lint: Ruff's
Drules (pydocstyle) enforce. Configure for the docstring style you pick (Google by default).
Enforcement: Ruff D100–D103 (missing docstrings on module/class/method/function) in CI.
Reasoning, step by step:
- Google style is readable in source, parseable by Sphinx (with
napoleon) and pdoc, well-supported by IDEs. - Pattern:
def charge_card(card: Card, amount: int, *, currency: Currency = Currency.USD) -> Receipt: """Charge the given card for the amount in the specified currency. The card must be active and have sufficient available credit. On success, returns a Receipt with the gateway-issued transaction ID. Args: card: The card to charge. Must be active. amount: Amount in the smallest unit of the currency (cents for USD). currency: Currency to charge in. Defaults to USD. Returns: A Receipt describing the successful charge. Raises: CardDeclined: If the gateway declines the charge. PaymentError: If the gateway is unreachable or returns an unexpected error. """
- Alternatives: NumPy style (more structured, heavier), reStructuredText (Sphinx default, more markup). Google style is our default.
Enforcement: Ruff convention = "google" under [tool.ruff.lint.pydocstyle]; the napoleon/pdoc parse runs in the docs build.
Reasoning, step by step:
- The signature documents the what. The docstring adds context.
"""Adds two integers."""ondef add(a: int, b: int) -> int:is useless."""Returns a stable hash for sharding. NOT cryptographically secure — use hashlib for security."""adds non-obvious information.- Heuristic: if deleting the docstring loses no information a caller needs, delete it.
Enforcement: review; reviewers reject docstrings that only restate the signature.
Reasoning, step by step:
- The first line is a one-sentence imperative summary. Ends in a period.
- Blank line separates summary from details. Sphinx/pdoc both rely on this.
- The first line appears in completion menus and indexes. Spend time on it.
- Multi-paragraph details for non-trivial cases. Be brief.
Enforcement: Ruff D205 (blank line after summary) and D400 (summary ends in a period) in CI.
Reasoning, step by step:
- Examples make docs actionable. Show how to use the function, not just what it does.
- Format:
def parse_iso_date(s: str) -> date: """Parse an ISO-8601 date. Example: >>> parse_iso_date("2025-01-01") datetime.date(2025, 1, 1) """
- doctest can execute these (
pytest --doctest-modules). Make them runnable. - For complex examples, prefer a sample function in a
_samplesmodule — referenced from the docstring, executed by tests.
Enforcement: pytest --doctest-modules in CI executes every doctest; a stale example fails the build.
Reasoning, step by step:
- Exception raising isn't in the signature. Docstring
Raises:is where it lives. - List exceptions the caller might reasonably catch. Don't list every
RuntimeErrorthat could theoretically escape. - Order: most-likely first.
- Specific exception types with what causes them:
Raises: ValueError if amount is non-positive.
Enforcement: review; a public raise the caller would catch must appear under Raises:, checked against the implementation.
Reasoning, step by step:
- Don't restate the type in the docstring.
Args: amount (int): The amount to charge.is noise — the signature saysamount: int. - Do add information the type can't: ranges, units, formats, special values.
Args: amount: Amount in the smallest currency unit (cents for USD). Must be positive.— the type system can't say "smallest unit" or "must be positive."- The compiler / mypy sees types. Humans see types and docstrings. Make the docstring add what types can't.
Enforcement: review; reject Args entries that restate the annotated type and add nothing.
Reasoning, step by step:
- The first line of a module is its docstring. Describes the module's responsibility.
- Sphinx/pdoc use this as the module description in generated docs.
- Pattern:
"""Payment processing: card tokenization, gateway dispatch, receipt construction. Public entry points: charge_card -- Charge a card for an amount. refund -- Refund a previously-charged transaction. """
Enforcement: Ruff D100 (missing module docstring) in CI.
Reasoning, step by step:
- Class docstring describes the class's purpose, when to use it, and any non-obvious lifecycle.
- For dataclasses, fields are documented in attribute annotations (and any explicit
field(metadata=...)). Don't restate trivially. - Non-dataclass attributes:
Attributes:section in the docstring. - Don't duplicate
__init__parameters in the class docstring if__init__has its own docstring. Pick one home.
Enforcement: Ruff D101 (missing class docstring) in CI; review for the one-home rule.
Reasoning, step by step:
warnings.warn(...)produces runtime signals (chapter 10 §10.7).- The docstring adds a
Deprecated:note explaining what to use instead. - Pattern:
def old_name(x: int) -> int: """Compute the old thing. Deprecated: use ``new_name`` instead. Will be removed in 3.0. """ warnings.warn("old_name is deprecated; use new_name", DeprecationWarning, stacklevel=2) return new_name(x)
Enforcement: review; a Deprecated: note must be paired with a warnings.warn(..., DeprecationWarning) call.
Reasoning, step by step:
- A comment that restates the next line is noise.
- A comment that explains a non-obvious why earns its line:
# Apple's smart-quote interferes with the parser; normalize.. - TODO comments need owner + date:
# TODO(omar 2026-08-01): remove after API v3 cutover. - Don't leave commented-out code. Delete it. Git remembers.
Enforcement: Ruff ERA001 flags commented-out code; TD rules require owner+date on TODOs; review for why-not-what.
Reasoning, step by step:
- Docs that don't build don't get read. CI builds docs on every PR.
pdoc— zero-config Python doc generator. Reads docstrings, produces HTML. Best default.mkdocs+mkdocstrings— for documentation sites with handwritten guides + auto-generated reference. Better for libraries with conceptual docs.- Sphinx + ReadTheDocs is the heavyweight, mature alternative. Pick if you need cross-references and complex structure.
Enforcement: CI step runs the doc build (pdoc or mkdocs build --strict); a broken build fails the PR.
- Type hints and what they document: chapter 03.
__all__as public-surface documentation: chapter 10.- Module README per package: chapter 12.