Docstrings: The Human-Readable Spec
Look at this function signature:
def process(text: str, limit: int) -> str: ...
What does this function do? You know it takes a string and an integer, and returns a string. But does it shorten the text? Remove certain characters? Translate it to another language? The types alone do not tell you.
Now look at this version:
def truncate(text: str, max_length: int) -> str:
"""Shorten text to max_length characters, adding '...' if truncated."""
...
The name helps (truncate is clearer than process). But the real answer is on the line inside triple quotes: """Shorten text to max_length characters, adding '...' if truncated.""" That line is called a docstring.
A docstring is a short description written inside the function that explains what the function does. The signature tells pyright and AI what types to expect. The docstring tells humans and AI what behavior to expect. Together, they form a complete specification.
How to Write a Docstring
A docstring goes on the very first line inside the function, wrapped in triple quotes ("""):
def truncate(text: str, max_length: int) -> str:
"""Shorten text to max_length characters, adding '...' if truncated."""
...
That is it. One line, triple quotes, a sentence describing the purpose.
Python stores the docstring as metadata. Tools like help(), your editor's hover tooltip, and AI all read it:
help(truncate)
Output:
Help on function truncate:
truncate(text: str, max_length: int) -> str
Shorten text to max_length characters, adding '...' if truncated.
Multi-line docstrings
If one sentence is not enough, add more detail after a blank line:
def calculate_reading_time(word_count: int, wpm: int = 250) -> float:
"""Calculate estimated reading time in minutes.
Divides the total word count by the words-per-minute reading speed.
Returns the result as a decimal number of minutes.
"""
...
The structure is: first line is a short summary, blank line, then additional detail. The first line should always make sense on its own, because some tools only show the first line.
The Most Important Rule: What, Not How
This is the one rule that separates good docstrings from bad ones.
A good docstring describes what the function does (its purpose):
def word_count(text: str) -> int:
"""Count the number of words in text."""
...
A bad docstring describes how it does it (its implementation):
def word_count(text: str) -> int:
"""Split text by spaces and return the length of the resulting list."""
...
Why is the "how" version bad? Because it restates the code. Anyone reading the body can see text.split() and len(). The docstring adds nothing. And if you later change the implementation (maybe using a regex instead of .split()), the docstring becomes a lie.
The "what" version answers the question: "Why does this function exist?" That answer stays true no matter how the body changes.
Here is a side-by-side comparison:
| What-style (good) | How-style (bad) | |
|---|---|---|
reading_time | "Calculate estimated reading time in minutes." | "Divides word_count by wpm and returns the result." |
format_title | "Strip whitespace and capitalize each word." | "Calls .strip() then .title() on the input string." |
is_valid_title | "Check whether a note title has at least 3 characters." | "Returns len(title) >= 3." |
Rule of thumb: if your docstring would need to change when you rewrite the body, it describes the "how." Rewrite it to describe the "what."
Why Precision Matters: A Vague Docstring Lets AI Guess
Look at this stub:
def format_note_header(title: str, author: str, word_count: int) -> str:
"""Create a header string for a note."""
...
If you ask AI to implement this, it might produce:
# AI's guess A:
return f"{title} by {author} ({word_count} words)"
# AI's guess B:
return f"[{author}] {title} - {word_count}w"
# AI's guess C:
return f"Title: {title}\nAuthor: {author}\nWords: {word_count}"
All three satisfy the vague docstring "Create a header string." Which one did you want? AI does not know, so it guesses. Now compare with a precise docstring:
def format_note_header(title: str, author: str, word_count: int) -> str:
"""Return a header in the format 'Title by Author (N words)'.
Example: format_note_header("My Note", "James", 350) returns "My Note by James (350 words)"
"""
...
Now AI knows the exact format. One docstring, one correct implementation. Precision eliminates guessing.
Signature + Docstring = Complete Specification
When you combine a typed signature with a precise what-style docstring, you have everything needed:
def note_preview(body: str, max_length: int = 50) -> str:
"""Return the first max_length characters of body, with '...' if truncated.
If body is shorter than max_length, return it unchanged.
Example: note_preview("Hello World", 5) returns "Hello..."
"""
...
This stub tells three audiences everything they need:
| Audience | What They Read | What They Learn |
|---|---|---|
| Pyright | Parameter types and return type | What types to check at every call site |
| AI | Signature + docstring + example | What to implement |
| Human | Docstring | What the function does without reading the body |
Emma pauses. "I once shipped a function where the docstring said 'returns a formatted string' but on edge cases it returned None. The tests only checked normal inputs. Be precise: if there are special cases, mention them in the docstring."
PRIMM-AI+ Practice: Evaluating Docstrings
Predict [AI-FREE]
Press Shift+Tab to enter Plan Mode before predicting.
Classify each docstring as what-style or how-style. Write your answers and a confidence score from 1 to 5 before checking.
def clean_tag(tag: str) -> str:
"""Remove leading and trailing whitespace from a tag."""
...
def merge_names(first: str, last: str) -> str:
"""Use an f-string to combine first and last with a space between them."""
...
def is_valid_title(title: str) -> bool:
"""Check whether a note title meets the minimum length requirement."""
...
Check your predictions
Docstring 1: clean_tag is what-style. It describes the purpose: removing whitespace. You could implement this with .strip(), a regex, or a loop, and the docstring would still be accurate.
Docstring 2: merge_names is how-style. It says "use an f-string," which is an implementation detail. A better version: "Combine first and last name into a single full name." The implementation could use +, f"", .join(), or format().
Docstring 3: is_valid_title is what-style. It describes the purpose without specifying how the check works. The implementation could check len(title) >= 3 or use a more complex validation.
If you got all three correct, you can distinguish specifications from implementation descriptions.
Run
Press Shift+Tab to exit Plan Mode.
Rewrite the how-style docstring from Docstring 2 as a what-style docstring. Then ask AI to implement all three functions. Compare: does the what-style docstring produce a different implementation than the how-style one?
Investigate
Look at AI's implementation for is_valid_title. What "minimum length" did AI choose? You did not specify a number in the docstring. Write one sentence explaining how you could make the docstring more precise to control AI's choice.
If you want to go deeper, in Claude Code, type: "Why did you choose that specific minimum length? What in my docstring led to that choice?"
Modify
Update the is_valid_title docstring to specify the exact minimum: "Check whether a note title has at least 3 characters after stripping whitespace." Re-prompt AI and see if the implementation changes.
Make [Mastery Gate]
Write docstrings for these 2 function stubs. Your docstrings must be what-style, specific enough for AI to generate the correct body, and not reveal the implementation:
def tag_count(tags: list[str]) -> int:
...
def format_reading_time(minutes: float) -> str:
...
Prompt AI to implement both. If AI's implementation matches your intent on the first try, your docstrings are good specifications.
Try With AI
If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.
Prompt 1: Let AI Evaluate Your Docstring
Here is my function stub:
def format_note_header(title: str, author: str, word_count: int) -> str:
"""Create a header string for a note."""
...
Is this docstring specific enough for you to implement the body
confidently? If not, what details are missing? Suggest a more
precise version.
Read AI's feedback. It will likely say the docstring is too vague: what format should the header be? Should it include all three values? In what order? This shows you how an underspecified docstring leads to guessing.
What you're learning: You are using AI as a specification reviewer. If AI cannot implement your docstring without guessing, the docstring needs more detail.
Prompt 2: Compare Implementations from Different Docstrings
Implement this function twice, using a different interpretation
each time:
def format_note_header(title: str, author: str, word_count: int) -> str:
"""Create a header string for a note."""
...
Show me two different implementations that both satisfy the
docstring. Then rewrite the docstring so only one implementation
is correct.
Compare AI's two versions. They will differ because the docstring was ambiguous. The rewritten docstring should produce exactly one correct implementation. This is the specification principle: precision eliminates ambiguity.
What you're learning: A vague docstring allows multiple correct implementations. A precise docstring narrows AI's output to exactly what you want. This is why "what" matters more than "how."
James looks at his note_preview stub. "The signature says what types. The docstring says what happens. The example shows exactly what the output should look like. That is... the whole specification?"
"That is the whole specification," Emma confirms. "You have typed parameters, return types, defaults, and now docstrings. Those are all the pieces."
"So what is left?"
"Using them all together. In Lesson 4, you write complete SmartNotes stubs with everything you have learned, write tests to verify the behavior, prompt AI to generate the bodies, and run pytest and pyright to confirm everything passes. That is the full TDG cycle with real functions."