When Python Code Speaks Clearly: The Hidden Contract Between Docstrings and Method Types
Hatched by Kai Nguyen
Aug 11, 2026
12 min read
1 views
91%
A method can be perfectly correct and still be badly designed if its name, documentation, and access to state tell three different stories. A class method that behaves like a factory, a static method that secretly depends on global state, or a docstring that promises more than the code can guarantee all create the same problem: the interface asks the reader to perform interpretive work that the program could have done for them.
This suggests a deeper question about Python design: How much of a programmer’s intention should be expressed in prose, and how much should be enforced by the structure of the code itself?
The answer is not either documentation or mechanics. Good interfaces use both. Documentation explains meaning, while method types and naming constrain what kinds of meaning are possible. Together, they form a communication system between the code’s present author and its future users.
The real purpose of an interface is to prevent misunderstanding
When people discuss documentation, they often imagine an after the fact explanation. The code exists first, and the docstring is added later so someone else can understand it. That model is too weak. A well written docstring is not merely commentary. It is part of the interface, just as the method signature is part of the interface.
Consider a function that creates a user account. Its name might be create_user, its parameters might be email and password, and its docstring might say that it validates the email, hashes the password, and persists the account. A reader forms a mental model from all three elements. If the function instead accepts a prehashed password, silently creates a temporary account, or performs a network call not mentioned anywhere, the problem is not simply inadequate explanation. The interface has become semantically dishonest.
Python offers several ways to reduce this dishonesty. An instance method receives self, which communicates that the operation is about a particular object. A class method receives cls, which communicates that the operation concerns the class or creates an instance through the class. A static method receives neither, which communicates that the operation does not need object or class state and is being placed in the class primarily for conceptual organization.
These distinctions are not just matters of taste. They are small, structural statements about dependency.
An instance method says: “My behavior depends on this object.”
A class method says: “My behavior depends on this class, or on the rules for constructing objects of this class.”
A static method says: “My behavior belongs near this concept, but does not depend on a particular object or class.”
A docstring then supplies the second layer: what the operation means, what it expects, and what it guarantees.
The strongest interface is not the one with the most explanation. It is the one in which the explanation and the structure make the same promise.
This gives us a useful test for design quality. Ask whether a reader can infer the method’s scope of authority from the method type, then confirm the details in the docstring. If the answer is yes, the interface is doing explanatory work before anyone reads the implementation.
Documentation has a shape, not just a word count
The discipline of writing docstrings reveals an important principle: clarity depends on hierarchy. A concise summary should come first, followed by a blank line and a fuller explanation when more detail is necessary. This is not merely a formatting convention. It reflects how people inspect code.
Most readers scan first. They want to know what a method is for before deciding whether to study its details. A summary line serves that first decision. The expanded description serves the second decision: whether the method’s behavior, assumptions, and effects are relevant to the task at hand.
Imagine a class called Invoice with these methods:
class Invoice:
@classmethod
def from_json(cls, payload):
"""Build an invoice from a JSON compatible mapping.
The payload must contain an invoice number and a list of line items.
Currency values are interpreted as decimal amounts, and missing tax
information defaults to zero.
"""
...
The first line answers the immediate question: what does this method do? The longer section answers questions that become important only after the reader decides to use it: what shape must the input have, how are missing values handled, and what does “from JSON” mean in this codebase?
A one line docstring is appropriate when the behavior is genuinely obvious. The important word is “obvious,” not “short.” A method named is_expired may need only a concise description if its semantics are straightforward. A method named normalize almost certainly does not. Normalize what? For what purpose? Is the operation destructive? Does it preserve ordering? Does it apply locale rules?
The danger of vague brevity is that it transfers uncertainty to the reader. A docstring such as """Normalize the data.""" appears efficient, but it forces the reader to inspect the implementation or guess. The text is short because the author omitted decisions, not because the behavior is simple.
Docstrings can also describe attributes and additional sections of a module or class, not only callable methods. This matters because objects expose more than operations. Their state, constants, and conceptual groupings are also part of the vocabulary through which other programmers understand the system. A documented attribute can tell a reader whether a value is stable, computed, optional, mutable, or safe to modify.
The underlying pattern is progressive disclosure:
- Start with the smallest truthful summary.
- Add detail only where the behavior is not recoverable from the name and signature.
- Explain constraints, side effects, defaults, and unusual cases.
- Keep the explanation close to the thing it describes.
This is why a docstring should not attempt to narrate every line of an implementation. The purpose is not to describe the code’s internal journey. The purpose is to state the contract that users need in order to use the code correctly without knowing its internal journey.
Method decorators are documentation that the interpreter can enforce
Now consider the complementary role of method types. Suppose a helper validates a pizza topping:
class Pizza:
@staticmethod
def valid_topping(name):
"""Return whether name is an accepted topping."""
return name in {"mushroom", "olive", "pepper"}
Why place this function inside Pizza rather than at module level? Perhaps because the concept belongs to pizza creation and callers naturally look for it there. The staticmethod communicates that the helper does not need a Pizza instance or the Pizza class. That fact is useful to the reader, and it is also preserved by the call behavior.
By contrast, a factory method needs the class because it should create an instance of whichever subclass invokes it:
class Pizza:
@classmethod
def from_menu_name(cls, name):
"""Create a pizza corresponding to a menu name."""
if name == "margherita":
return cls(["tomato", "mozzarella", "basil"])
if name == "vegetarian":
return cls(["pepper", "onion", "mushroom"])
raise ValueError(f"Unknown menu name: {name}")
The use of cls is not decorative. It preserves the class context. If a specialized pizza class inherits this method, the factory can construct the specialized class rather than hard coding the parent class. The method type therefore communicates and supports a construction policy.
An instance method has a different kind of authority:
class Pizza:
def price(self):
"""Return the current price of this pizza."""
return self.base_price + self.topping_cost()
This method is about one particular pizza. It can inspect and modify instance state because its meaning depends on the specific object. Recasting it as a static method would not make it simpler. It would merely hide the dependency, perhaps by requiring the caller to pass an object manually or by reaching into global data.
These choices create what we might call a dependency gradient:
- Instance methods have the narrowest context, one object.
- Class methods have a broader context, the class and its construction rules.
- Static methods have no implicit object context and function as namespaced utilities.
A method should be placed at the lowest level of context that can truthfully support its behavior. If a function does not need self, do not give it the appearance of operating on an instance. If it needs cls to preserve polymorphic construction, do not hide that relationship in a module level function that hard codes a class.
This principle can expose design problems early. If a static method keeps asking for an object as an argument, it may really be an instance method. If a class method never uses cls, it may be a static method or a module level function. If an instance method never touches instance state, it may be carrying a misleading object oriented costume.
The decorator is therefore a kind of executable annotation. It tells humans what the method needs, and it lets Python establish the corresponding calling convention. The language does not prevent every misuse, but it removes several easy opportunities for accidental misuse.
The contract has two layers: meaning and permission
A useful way to combine documentation and method design is to distinguish between semantic contracts and authority contracts.
A semantic contract answers questions such as:
- What does this operation do?
- What inputs are valid?
- What does it return?
- What exceptions or side effects should the caller expect?
An authority contract answers a different set of questions:
- Which object may this operation inspect?
- May it change instance state?
- May it change class state?
- Is it a construction pathway?
- Is it merely a related utility?
Docstrings primarily express the first contract. Method types primarily express the second. A robust design aligns them.
For example, suppose a class method’s docstring says “Update the shipping address of this order.” The wording suggests a particular order instance, but the method has no self. That mismatch forces the reader to wonder whether the method updates every order, creates a revised order, or uses some hidden identifier. The prose and structure disagree about the object of the action.
Or suppose a static method’s docstring says “Return the default timeout for this class.” The word “this class” implies class state, yet a static method has no cls. The method may still work if it reads a constant through a global name, but its declared authority and actual behavior are misaligned.
We can represent the alignment as a simple matrix:
| Question | Best expressed by |
|---|---|
| What does it do? | Summary docstring |
| What are the edge cases? | Expanded docstring |
| What state does it need? | Method type and parameters |
| What may it change? | Method type, signature, and documentation |
| How is an object created? | Class method and constructor documentation |
| Why is it located here? | Naming, namespace, and concise explanation |
The value of this matrix is not bureaucratic completeness. It helps identify where a reader should look for an answer. A method that requires a long explanation to clarify why it has access to self may have been given the wrong scope. A method whose docstring must explain that it does not mutate anything may benefit from a static method or a design with stronger immutable boundaries.
This also explains why well chosen method types improve testing. A static method with no hidden instance or class dependency can often be tested as an ordinary function. A class method can be tested as a construction policy, including whether subclasses are respected. An instance method can be tested against explicit object state. The clearer the authority boundary, the smaller the setup required to test the behavior.
Testability is often the shadow cast by good boundaries. When a method needs less invisible context, it needs less elaborate scaffolding in a test.
Designing interfaces that explain themselves
Self explaining code does not mean code with no documentation. It means code whose documentation can be precise because the structure has already removed avoidable ambiguity.
Start by identifying the method’s true subject. Is it one object, a class of objects, or a concept associated with the class? That answer usually points toward instance, class, or static method. Next, identify the method’s public promise in one sentence. If the sentence cannot fit naturally into a clear summary line, the behavior may be doing too much, or its purpose may not yet be understood.
Then document the parts a competent caller cannot infer safely. These often include accepted formats, default behavior, mutation, ordering, units, failure modes, and whether a factory preserves subclass behavior. Avoid describing private implementation details unless they affect the contract. “Uses a dictionary internally” is rarely useful. “Raises ValueError when the currency code is unsupported” is useful.
Finally, inspect the interface for contradictions. A practical review can ask:
- Does the method type match the state it reads or changes?
- Does the name match the outcome rather than the mechanism?
- Does the first docstring line describe the result in plain language?
- Would a caller need to inspect the implementation to discover a surprising side effect?
- Could a subclass or future maintainer rely on the stated behavior?
Suppose we refactor a parsing API:
class Report:
@classmethod
def from_csv(cls, text):
"""Build a report from CSV text.
The first row supplies column names. Empty rows are ignored, and
malformed records raise ValueError rather than being silently skipped.
"""
...
@staticmethod
def is_valid_column_name(name):
"""Return whether name is a nonempty column name without whitespace."""
...
The design now communicates several things before the implementation is read. from_csv is a named alternative constructor, not an operation on an existing report. is_valid_column_name is a related rule that needs neither report state nor class state. The summaries make the methods discoverable, while the expanded text makes important policy decisions visible.
Notice what this design does not do. It does not attempt to make every behavior impossible to misuse. Python remains flexible, and no decorator can replace judgment. The goal is more practical: make the intended use the easiest use, make accidental use conspicuous, and make the contract close enough to the code that maintenance does not require archaeological investigation.
Key Takeaways
- Choose method types by dependency, not preference. Use an instance method for behavior tied to one object, a class method for class aware behavior or alternative construction, and a static method for a related utility with no implicit state.
- Treat the first docstring line as an interface label. It should state the method’s purpose plainly and truthfully, not merely repeat its name.
- Use expanded documentation for decisions callers cannot infer. Explain inputs, defaults, mutations, side effects, exceptions, and important edge cases.
- Look for mismatches between prose and authority. If the docstring talks about an instance but the method has no
self, or talks about class policy but has nocls, reconsider the design. - Use structure to reduce documentation burden. A well scoped method needs fewer warnings because its signature and binding behavior already communicate part of the contract.
The deepest lesson is that documentation and architecture are not separate acts. One speaks in sentences, the other in permissions. A docstring tells future readers what a piece of code means; a method type tells Python and those readers what context the code is allowed to assume.
When those two forms of communication agree, an interface becomes more than readable. It becomes resistant to misreading. The next programmer does not have to reconstruct intention from scattered clues because intention has been placed in the name, the binding, the signature, and the contract together.
That is the real standard for self documenting code: not code that says everything, and not code that says nothing, but code whose shape makes the truth easier to say.
Sources
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 🐣