Skip to main content

Docstrings: The Human-Readable Spec

James has a function signature: def truncate(text: str, max_length: int) -> str. The types are clear. But what should the function actually do? Shorten the text? Raise an error? Pad it with spaces?

Emma asks: "If I read only the signature, do I know whether truncate adds ... at the end or just cuts off?"

James shakes his head. "The types say it takes a string and an integer and returns a string. They don't say what happens."

"That is where the docstring goes." Emma types three quotes under the def line. "The signature says what types. The docstring says what happens. Together, they are a complete specification."


Writing a Docstring

A docstring is a string that sits on the first line inside a function body. It uses triple quotes ("""):

def truncate(text: str, max_length: int) -> str:
"""Shorten text to max_length characters, adding '...' if truncated."""
...

Output (from help()):

help(truncate)
Help on function truncate:

truncate(text: str, max_length: int) -> str
Shorten text to max_length characters, adding '...' if truncated.

Python stores the docstring as metadata. Tools like help(), your editor's hover tooltip, and AI all read it. This single sentence tells everyone what the function does.

For longer descriptions, use a multi-line docstring:

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.
"""
return word_count / wpm

Output:

2.0

The first line is a summary. The blank line separates it from additional detail. The detail explains behavior that is not obvious from the signature alone.


What vs How

A good docstring describes what the function does. A bad docstring describes how it does it.

StyleDocstringProblem
What (good)"Calculate estimated reading time in minutes."Tells you the purpose
How (bad)"Divides word_count by wpm and returns the result."Restates the code

The "how" version adds nothing. Anyone reading the body can see word_count / wpm. The "what" version answers the question "Why does this function exist?"

Here are three examples that contrast what-style with how-style:

# GOOD: what-style
def word_count(text: str) -> int:
"""Count the number of words in text."""
...

# BAD: how-style (restates the code)
def word_count(text: str) -> int:
"""Split text by spaces and return the length of the resulting list."""
...

The good version tells you the function's purpose. The bad version tells you the implementation, which might change (what if you switch from .split() to a regex?). Describe the destination, not the route.


Signature + Docstring = Specification

When you combine a typed signature with a what-style docstring, you have a complete specification:

def note_preview(body: str, max_length: int = 50) -> str:
"""Return the first max_length characters of body, with '...' if truncated."""
...

This stub tells AI everything it needs:

  • Input types: a string and an integer with a default of 50
  • Return type: a string
  • Behavior: return the first N characters; append "..." if the text was shortened

AI can generate the body from this specification alone. Pyright can verify every call matches the types. A human reader can understand the function without reading the body. That is the power of a well-written spec.

Emma pauses. "I once shipped a function where the return type said str but it returned None on edge cases. The tests only checked normal inputs. Pyright would have caught the None return if I had annotated it properly. The docstring said 'returns a formatted string' but it should have said 'returns a formatted string, or raises ValueError for empty input.' Be precise in your docstrings."


Putting It Together

Here are three SmartNotes function stubs with complete specifications:

def word_count(text: str) -> int:
"""Count the number of words in text."""
...

def format_title(title: str) -> str:
"""Strip whitespace and capitalize each word."""
...

def note_preview(body: str, max_length: int = 50) -> str:
"""Return the first max_length characters of body, with '...' if truncated."""
...

Each stub has types (the contract for pyright) and a docstring (the contract for humans and AI). In Lesson 4, you write tests for these stubs and prompt AI to generate the implementations.


PRIMM-AI+ Practice: Evaluating Docstrings

Predict [AI-FREE]

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

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.

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

Opening Claude Code

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."


Key Takeaways

  1. A docstring describes what, not how. "Count the number of words in text" is a what-style specification. "Split text by spaces and return the length" is a how-style implementation description. Write the what.

  2. Signature + docstring = specification. The types tell pyright what to check. The docstring tells AI (and humans) what to build. Together they form a complete contract.

  3. Precision eliminates ambiguity. If AI can interpret your docstring two ways, it will guess. Make docstrings specific enough that only one implementation is correct.


Looking Ahead

You now have all the pieces of a function specification: typed parameters, return types, defaults, and docstrings. In Lesson 4, you put them all together in a full TDG cycle: write stubs with complete specs, write tests, prompt AI to generate the bodies, and verify everything passes. This is the workflow you will use for every function from here forward.