When Code Speaks Design: How Docstrings Teach Your Classes to Behave

Kai Nguyen

Hatched by Kai Nguyen

Apr 14, 2026

9 min read

85%

0

Why your docstrings are not optional commentary but the secret engine of good design

What if the line between good architecture and bad architecture was not found only in code but in the sentences that sit above it? Many programmers treat docstrings as afterthoughts, tiny notes to future selves. That mistake hides a deeper confusion: documentation is not a passive record of intent, it is an active design tool. When written with discipline, docstrings expose design choices, enforce boundaries, and reveal smells faster than any static analysis tool.

This article argues a simple claim: docstrings are design artifacts. If you write docstrings as if they were legal contracts, they will improve your code. If you write them as vague rambling, they will mask design rot. I will show how the five core principles of object oriented design map directly to practical docstring practice, and I will give an actionable framework you can apply now to turn documentation into a first class part of your architecture.


The tension: clarity versus drift

Software systems drift. What began as a single purpose class becomes a grab bag of responsibilities. Interfaces bloat. Tests stop telling you how to use an object; they only tell you how it behaves in the current code base. The tension is between two impulses: minimize upfront words to avoid duplication, and add explanation to prevent misuse. Too little prose and you force every reader to infer intent from code. Too much prose and the text goes stale or conflicts with the implementation.

Docstrings sit at the heart of that tension. They can either be a source of clarity, a contract that makes it safe to evolve code, or they can be wishful comments that create cognitive overhead. The choice about how to write a docstring is a design decision. It can either keep the Single Responsibility Principle alive, or accelerate its death.

Consider this small contrast: a method with no docstring, and one with a precise summary line and a usage example. The first requires a reader to open the implementation, line by line, to infer intent. The second tells the reader what to expect, how it will behave, and what not to do. When the second is present, refactoring becomes safer because the docstring serves as an explicit contract you can check the code against.


Mapping design principles to docstring practice

If we accept that docstrings are design artifacts, then every structural design principle both expects and benefits from a certain docstring practice. Below I present a direct mapping between five classical object oriented design principles and practical documentation habits. Think of this as a translation guide. When a docstring follows these patterns, it helps enforce the associated principle. When it does not, you should suspect a design smell.

Single Responsibility: document the role, not the steps

Principle: a class or module should have one reason to change. In prose, that reason should be the entire summary.

Docstring habit: start with a concise summary line that names the role of the class, followed by one paragraph that explains the single responsibility. If the class encapsulates multiple behaviors, call that out as a smell and extract responsibilities into smaller objects.

Example:

class InvoicePrinter:
    """Render invoices for printing and export.

    This class focuses on formatting an invoice for printing and for PDF export.
    It does not compute totals or apply business rules. Use InvoiceCalculator
    for computations, and then pass the result to this class for rendering.
    """
    ...

Why it helps: the docstring names the boundary between formatting and computation. When you later see calculation code creeping into this class, the docstring is the first line of defense in a code review.

Open and Closed: document intended extension points

Principle: classes should be open for extension and closed for modification. That is easier to achieve if you declare which parts are stable and which parts are extension points.

Docstring habit: explicitly list which methods are stable public API and which are extension hooks. Provide the contract for each hook: expected input, returned value, and side effects.

Example:

class Cache:
    """In memory cache with customizable eviction.

    Public API: .get(key), .set(key, value), .clear()
    Extension points: override _evict() to change eviction policy. _evict
    must accept no arguments and may mutate internal state.
    """
    ...

Why it helps: a reviewer can see whether a change modifies a declared extension point or silently breaks the closed part of the API. It also gives implementers a clear protocol to follow.

Liskov Substitution: document behavioral expectations, not only signatures

Principle: subclasses must be substitutable for their base classes. This is a behavioral contract, not just a type signature.

Docstring habit: for base classes and interfaces, write docstrings that describe not only parameters and returns but also invariants, failure modes, and performance expectations. For methods that must preserve particular properties, say so explicitly.

Example:

class PaymentProcessor:
    """Abstract payment processor.

    Implementations must be atomic: process(amount) either completes the
    transaction and returns a confirmation id, or raises an exception and
    leaves external state unchanged. Implementations must not retry
    automatically on transient network errors.
    """
    def process(self, amount):
        raise NotImplementedError

Why it helps: a subclass author who reads this will avoid surprising behavior that breaks callers. The docstring reduces the chance of silent contract violation.

Interface Segregation: document focused, small interfaces

Principle: prefer many client specific interfaces over one general purpose interface. Documentation should mirror that granularity.

Docstring habit: when an object implements multiple roles, document each role separately as an attribute level docstring or with small helper classes. Avoid a single massive docstring that lists a dozen unrelated responsibilities.

Example: if a class handles persistence and validation, prefer two small classes with focused docstrings. If you must keep both in one class, use attribute docstrings to explain separate roles.

Why it helps: readers will not be overwhelmed by a monolith of responsibilities. Small docstrings make it obvious which clients can depend on which parts of the object.

Dependency Inversion: document abstractions not implementations

Principle: high level modules should not depend on low level modules. Both should depend on abstractions. Docstrings should therefore describe the abstraction in terms of observable behavior and not implementation details.

Docstring habit: for interfaces and abstract base classes, avoid mentioning concrete implementations. Describe observable behavior, side effects, and performance expectations.

Example:

class EventStore:
    """Append only event store abstraction.

    Methods should accept domain events and persist them in order. Implementors
    may use files, databases, or in memory lists. Do not rely on any database
    specific features in client code.
    """
    ...

Why it helps: clients write to the abstraction rather than to a concrete behavior, making substitution and testing easier.


A design and documentation loop: write docs to reveal smells

Here is a practical workflow I use when refactoring or designing new code. Treat docstrings as an instrument for discovery, not only a byproduct.

  1. Before you code, write the summary line and two or three sentences describing the responsibilities and the public API you expect. This forces clarity about scope. If you cannot describe the role in a short paragraph, you probably have more than one responsibility.

  2. Implement a minimal version following that docstring. The docstring becomes a testable contract. If implementing the contract forces awkward coupling, revise the docstring and the design before growing the code base.

  3. During code review, require the author to update docstrings when behavior changes. If the docstring is vague, require refinement. If tests pass but the docstring says something stronger, question which is authoritative.

  4. Use docstrings in your automated checks. Tools can parse summary lines and attribute docstrings. If a public method has no docstring or no summary line, flag it. This incentivizes clear documentation as part of the build.

This loop creates a feedback mechanism: docstrings reveal where responsibilities are creeping, and code changes force a revision of the docstring or a refactor. In a well functioning team, documentation is part of the design contract.

Documentation is not an optional polish. It is a contract that makes design choices explicit and evolvable.


Concrete examples that separate intent from implementation

Imagine a code base for processing orders. Below are two designs that illustrate how docstrings steer architecture.

Bad example: a monolithic OrderManager with a long docstring that lists everything it does. The summary is vague. The methods have no clear behavioral contracts. Over time, people add payment, inventory, shipping, and analytics into this class because there is no declared boundary.

Good example: split responsibilities into small classes with precise docstrings. Each class documents its public API and its invariants. Here are snippets.

class OrderRepository:
    """Persist and retrieve order state.

    Responsibilities: store order snapshots and event history. Methods return
    plain data structures suitable for business logic. This class must not
    apply business rules or trigger external side effects.
    """

class PaymentGateway:
    """Process payments for orders.

    process(payment_info) returns a tuple (confirmation_id, metadata). On
    failure raises PaymentError and leaves external systems unchanged.
    Implementations must not retry automatically.
    """

class OrderService:
    """Coordinate order processing using small focused components.

    This high level class orchestrates OrderRepository and PaymentGateway.
    It handles retries and compensating actions. It depends on abstractions
    and not on concrete implementations.
    """

Because each piece documents what it is responsible for, it becomes much harder for a future change to fold unrelated functionality into an existing component. The docs function as a psychological and practical boundary.


Key Takeaways

  • Write a clear summary line for every public class and function using triple double quotes. This is your single sentence contract for the role it plays.
  • Use the docstring to declare extension points and invariants. If you cannot state what must not change when extending, you do not have a stable API.
  • Treat docstrings as part of the reviewer checklist. If the implementation and the docstring disagree, require the author to resolve the conflict before merging.
  • Split responsibilities until each docstring comfortably fits a short paragraph. If the docstring runs long, split the class into smaller units.
  • Use docstrings to express behavior expectations that go beyond signatures: atomicity, error semantics, performance expectations, and side effects.

Conclusion: make your code speak clearly about itself

Design is not only about how code is organized. It is also about how code communicates its purpose, its boundaries, and its promises. Docstrings are the human readable face of those promises. When you treat documentation as a first class part of design, you gain two advantages. First, you make intent explicit, which makes refactoring safer and faster. Second, you externalize the contract so that new contributors can grasp the architecture without reading every implementation.

Next time you feel resistance to writing a docstring, treat that feeling as a design alarm. If you cannot explain a class in a few sentences, that is not a documentation problem. It is a design problem. Fix the design. The prose will then write itself.

Clear docstrings do not make code redundant. They make code accountable.

Sources

← Back to Library

Hatch New Ideas with Glasp AI 🐣

Glasp AI allows you to hatch new ideas based on your curated content. Let's curate and create with Glasp AI :)

Start Hatching 🐣