Exercises — Chapter 24: Code Documentation

Documentation is writing, and writing is learned by writing. These exercises ask you to produce and revise documentation — to delete what-comments, write why-comments, draft docstrings, and judge how much is enough. You will not write code logic; you work on the comments, docstrings, names, and types layered onto code that already exists. Difficulty: ⭐ (warm-up) to ⭐⭐⭐⭐ (extension).

How to use these: Do them in a plain text editor, not in your head. Where a task says "rewrite," actually type the rewrite — the gap between recognizing a bad comment and writing a good one is where the skill lives. Selected solutions and self-assessment rubrics follow the relevant parts; for open-ended tasks, grade yourself against the rubric provided.


Part A — Analyze This ⭐

For each, identify what's right or wrong with the documentation. Name the principle (§24.2 why-not-what, §24.3 docstring contract, §24.5 self-documenting, §24.6 spectrum) — don't just react.

A1. What is wrong with this comment? Name the failure precisely.

total = price * quantity   # multiply price by quantity

A2. This comment is good. Say exactly why — what does it give a reader that the code can't?

# Retry up to 3 times: the payment gateway occasionally returns a transient
# 503 under load, and a single retry clears it ~95% of the time. See incident #402.
for attempt in range(3):
    ...

A3. A file has a comment on all 60 of its lines. Without seeing the file, what failure is almost certainly present, and what's the second-order harm (beyond wasted space)?

A4. Is this a comment, a docstring, or something that should be a name? What should the author have done instead?

x = 86400   # number of seconds in a day

A5. Read this docstring as a caller who wants to use the function. What's missing from its contract?

def withdraw(account, amount):
    """Withdraw money from the account."""
    ...

A6. This comment was true when written. What category of failure does it now represent, and why is it worse than having no comment at all?

# Returns the user's age in years
def get_user_info(user_id):
    return db.fetch(user_id).date_of_birth

A7. Two functions in the same codebase. What documentation problem do they show together (not individually)?

def fetch_user(id): """Get a user by their ID."""
def get_account(account_id): """:param account_id: the account identifier"""

A8. A pull request deletes a block of commented-out code and the reviewer objects: "Don't delete that, we might need it." Who's right, and what's the correct way to preserve what matters about that code?


Part B — Revise This ⭐⭐

Rewrite the documentation. Give the corrected comments/docstring/names. The scenario tells you enough to infer a plausible why — invent a realistic one where needed and state your assumption.

B1. (Rewrite these what-comments as why-comments — or delete them.) Here is a function with five comments, every one of them narrating the what. For each comment, decide: delete it (pure narration), or replace it with a plausible why (if the line plausibly hides a decision). Produce the rewritten function.

def clean_emails(rows):
    # create an empty list
    cleaned = []
    # loop through the rows
    for row in rows[1:]:
        # get the email from column 2
        email = row[2].strip().lower()
        # check if @ is in the email
        if "@" in email:
            # add it to the list
            cleaned.append(email)
    # return the list
    return cleaned

(Hint: at least one of these lines hides a real why. rows[1:] skips something. "@" in email is a suspiciously loose check. The strip/lower might be load-bearing. Find the whys; delete the rest.)

B2. (Rewrite these what-comments as why-comments.) Same task, harder code. The comments narrate; the real whys are unwritten.

def score(values):
    # sort the values
    values = sorted(values)
    # remove the first and last
    values = values[1:-1]
    # calculate the average
    return sum(values) / len(values)

(What's the why behind dropping the first and last after sorting? Name it, and write the one comment this function actually needs.)

B3. (Write the docstring.) This function has no docstring. Write a complete one in Python / PEP 257 / Google style (summary, Args, Returns, Raises) such that a caller could use it without reading the body.

def split_name(full_name):
    parts = full_name.strip().split()
    if len(parts) < 2:
        raise ValueError(f"Expected at least first and last name, got: {full_name!r}")
    return parts[0], parts[-1]   # first, last — ignores middle names

B4. (Write the docstring.) Write a JSDoc docstring for this JavaScript function — summary, @param (with types and the optional/default marker), @returns, @throws.

function truncate(text, maxLength = 80) {
  if (typeof text !== "string") throw new TypeError("text must be a string");
  if (text.length <= maxLength) return text;
  return text.slice(0, maxLength - 1) + "…";
}

B5. (Self-document: kill the comments by fixing the code.) Rewrite this so the three comments become unnecessary — through better names and a named constant. Delete every comment the rewrite makes redundant.

# t is the temperature in celsius
t = read_sensor()
# convert to fahrenheit
f = t * 9 / 5 + 32
# alert if above the safety limit
if f > 104:
    trigger_alarm()

B6. (Add type hints as documentation.) Add type hints to these signatures so each one documents the shape of its data. State any assumption you make about a parameter's type.

def parse(text, base=None): ...
def average(numbers): ...
def find_user(user_id, include_inactive=False): ...

B7. (Cut to the calibrated center.) This function is over-documented — narration everywhere, plus one real why buried in the noise, plus a stale comment. Cut it to sparse-and-load-bearing: delete the narration, keep/fix the real why, fix the lie. Produce the result.

def apply_discount(price, code):
    # define the discount
    discount = 0.0
    # check the code
    if code == "SAVE10":
        # set discount to 10 percent
        discount = 0.10
    # SAVE20 gives 20 percent          <-- but the line below says 0.25
    elif code == "SAVE20":
        discount = 0.25
    # multiply price by 1 minus discount
    # Round to 2 decimals because currency can't have fractional cents,
    # and banker's rounding here would mismatch the billing system. Bug #91.
    return round(price * (1 - discount), 2)

Part C — Write This ⭐⭐–⭐⭐⭐

Produce documentation from a scenario. These are short writing tasks — the output is comments, a docstring, or a short prose note.

C1. (Write the why.) You wrote this line three months ago and you're now staring at it with no memory of why. Invent a realistic, specific why (the kind you'd actually have had) and write the comment that would have saved you:

time.sleep(0.25)

Write two different plausible why-comments for it (e.g., one about rate-limiting, one about a race condition) to feel how the same line can need very different documentation depending on intent.

C2. (Document a whole small function, every layer.) Here is an undocumented function. Produce the fully documented version applying every tool in the chapter: a PEP 257 docstring, type hints on the signature, at least one why-comment, and at least one rename to a self-documenting name. State your assumptions.

def f(s, n):
    r = []
    for w in s.split():
        if len(w) >= n:
            r.append(w)
    return r

C3. (Write a TODO that helps.) You've shipped a function that doesn't yet handle timezones, and you know it. Write a TODO comment that does the job well per §24.6: it should make the gap findable, say what is missing and why it's deferred, and (in form) link a ticket. Then write the bad version of the same TODO (vague, unfindable) and say in one sentence what makes yours better.

C4. (The "letter to your future self" test.) Pick a function or script you actually wrote in the last year and open it. Write a short note (150–200 words) to your six-months-ago self listing the three questions you most wish that code had answered — the whys you had to re-derive or guess. Then turn each of the three into the comment that should have been there.

C5. (Translate a why up a level.) You wrote a line-comment that reads: # Using a list, not a set: order matters in the export downstream, even though a set would dedupe for free. Your reviewer says, "This is really a design decision — it belongs somewhere bigger." Rewrite the same why as the opening of a one-paragraph note suitable for a commit message or a (Chapter 25) ADR, preserving the decision and the rejected alternative. (This previews how a line-why scales up.)


Part D — Synthesis & Critical Thinking ⭐⭐⭐

D1. (Find the flaw in the rule.) A junior developer takes "comment the why, not the what" to an extreme and deletes all comments that describe what the code does — including a docstring's parameter descriptions ("but that just says what the parameter is!"). Explain precisely where their logic breaks. (Hint: distinguish the internal what a comment narrates from the external what a docstring must state for a caller, and recall §24.4's what/why boundary for type hints.)

D2. (Reconcile two principles.) §24.5 says the best comment is often no comment (make the code self-documenting). §24.2 says capture the why in a comment. A teammate claims these contradict: "Either I should write comments or I shouldn't." Resolve the apparent contradiction in a short paragraph, using the what/why distinction. Which whats should vanish into the code, and which whys must stay in comments?

D3. (Translate for three audiences.) Take this single design decision and document it for three different readers, in the appropriate form for each:

Decision: the cache entries expire after exactly 90 seconds, chosen because the upstream data refreshes on a 60-second cycle and 90s guarantees we never serve data more than one cycle stale while still cutting upstream calls by ~85%. - (a) As an inline comment next to CACHE_TTL_SECONDS = 90 (one or two lines). - (b) As the Args/note portion of a docstring for the function that reads the cache (what a caller needs to know about staleness). - (c) As the opening of an ADR / commit-message paragraph (the full decision and its rejected alternative — e.g., why not 60s, why not 300s). Then say in one sentence what changed between the three and why — this is Chapter 2 (audience) and Chapter 7 (register) operating on documentation.

D4. (Judge the spectrum.) You inherit two modules. Module A: zero comments, but every name is intention-revealing, every constant is named, every public function has a typed signature and a docstring. Module B: a comment on nearly every line, no type hints, single-letter variable names, three docstring styles. Rank them for documentation quality, justify the ranking against the spectrum (§24.6), and name the one thing Module A might still be missing that no amount of good naming can supply.

D5. (The cost argument.) Make the business case, in one tight paragraph, for why-comments to a skeptical manager who thinks "documentation is a waste of developer time — the code is the documentation." Use the future-self/maintainer argument (§24.1), the re-derivation cost, and the doc-drift point honestly (don't oversell — concede that what-comments and over-documentation are a waste). This is a persuasion task: lead with the cost, not the principle (a Chapter 20 / Chapter 3 move).


Part M — Mixed Practice (Interleaved) ⭐⭐–⭐⭐⭐

These mix Chapter 24 with earlier chapters, so you must choose the right tool — not every problem here is a documentation problem.

M1. (Ch 24 × Ch 3.) Here is a comment and a sentence from a project's prose docs. One needs the clarity treatment from Chapter 3 (cut the fog); one needs the §24.2 treatment (it's narrating the what). Diagnose each correctly and fix both. - Comment: # in order to be able to check whether or not the value is present, we check if it is not none - Prose: "It is important to note that the function, in most cases, will generally return a list of results."

M2. (Ch 24 × Ch 8.) A docstring's description paragraph is choppy and out of order:

"Raises ValueError on bad input. It parses a date. The base argument is the reference time. It returns a datetime. Relative dates use the base." Reorder it into a coherent docstring description using Chapter 8's given-new / topic-first discipline (summary first, then the natural order params → behavior → return → raises). Then format it as a proper PEP 257 docstring.

M3. (Ch 24 × Ch 7.) A codebase names the same concept three ways across three files: user, account, and member — all referring to the identical entity. Name the Chapter 7 principle this violates, explain why it's worse in code than in prose, and state the fix. Is this a comment problem or a naming problem?

M4. (Ch 24 × Ch 2.) You're writing a docstring for a function in a public library that both expert developers and relative beginners will call. How does Chapter 2's audience analysis change what you put in the docstring versus the in-body comments? (Hint: who reads each, and what does each audience already know?)

M5. (Ch 24 × Ch 12.) You're reviewing a colleague's pull request. They've added good code but documented it badly: three what-comments, a magic number, a missing docstring, and a genuinely insightful why-comment. Write your code-review feedback on the documentation — applying Chapter 12's give-feedback skill (specific, kind, separate praise from critique). Keep the good, fix the bad, and don't be a jerk about it. (This previews Chapter 34.)

M6. (Ch 24 × Ch 6.) This comment is grammatically broken and commenting the wrong thing:

# this loop it iterate over the list and incrementing each of the counter
for c in counters: c.increment()

Fix both problems: the Chapter 6 sentence-level errors (it's barely a sentence) and the Chapter 24 problem (is this even the comment the line needs?). State which fix mattered more and why.


Part E — Extension ⭐⭐⭐⭐

For motivated readers and the Deep Dive track.

E1. (Documentation archaeology.) Find a real open-source function on GitHub (something you use). Read its comments and docstring critically against this chapter. Write a 250-word critique: Where does it comment the why well? Where does it narrate the what? Is the docstring a complete contract? Are there magic numbers crying out for names? Is anything stale? Be specific — quote the lines. (This is "reading as a writer," Chapter 39, applied to code.)

E2. (The drift experiment.) Take a well-commented function (yours or found) and make a small change to its logic — change a return type, flip a condition, alter a default. Now find every comment, docstring, and type hint that your one change made false. Count them. Write a paragraph on what this exercise teaches about the maintenance cost of documentation and why "coupled" documentation (names, types, generated docs) is safer than "decoupled" prose (§24.4, §24.6).

E3. (Design a team comment convention.) Draft a one-page "documentation style" section for a team's contributing guide (it'll resurface in Chapter 25's CONTRIBUTING.md). Cover: when to comment (the why-rule), the chosen docstring style (pick one, justify briefly), the rule on magic numbers and naming, the policy on commented-out code and TODOs, and the doc-drift rule (change comments with code). Keep it to one page — a style guide too long to read enforces nothing (Chapter 23). Model the brevity you preach.


Self-Assessment Rubrics & Selected Solutions

Full solutions to selected items; rubrics for the open-ended ones.

A1 — solution. Pure what-narration: # multiply price by quantity restates price * quantity in English and adds zero information. The failure is "commenting the what" (§24.2) — the software twin of "it is important to note that" (Ch 3). Fix: delete it. The line is self-documenting once total, price, and quantity are well named (which they are).

A2 — solution. It captures four things the code cannot express: the intent (retry on transient failure), the reason for the magic number 3 (a single retry clears it most of the time), the evidence (~95%, incident #402), and an implicit warning (don't "simplify" the retry away — it's load-bearing). This is a model why-comment: every clause carries a fact that exists nowhere in the code.

A4 — solution. It should have been a named constant, not a comment: SECONDS_PER_DAY = 86400. The comment is compensating for an unnamed magic number; naming it documents the what permanently, everywhere the value is used, and can't drift (§24.5). The comment fixed the symptom; the name fixes the cause.

A5 — solution. The docstring states the summary but not the contract. Missing: what account and amount are (types, units — dollars? cents?); what it returns (the new balance? nothing? a receipt?); and — critically for a withdraw — what it raises (insufficient funds? a closed account? a negative amount?). A caller cannot use this safely from the docstring alone, which is the docstring's whole job (§24.3).

A6 — solution. It's a stale comment / doc drift (§24.6): the comment says "age in years" but the function now returns a date of birth — and the function is even misnamed (get_user_info returning one field). It's worse than no comment because no comment would force you to read the code (always true), whereas this comment makes you trust a falsehood and skip the code. Fix: correct the comment, fix the function/return name, and ideally let a type hint (-> date) carry the truth where a tool checks it.

B1 — solution (model).

def clean_emails(rows):
    cleaned = []
    # Skip rows[0]: it's the CSV header, not data.
    for row in rows[1:]:
        # Normalize so duplicates that differ only by case/whitespace collapse.
        email = row[2].strip().lower()
        # Loose "@" check by design: we only reject the obviously-broken here;
        # full RFC-5322 validation happens later in the pipeline.
        if "@" in email:
            cleaned.append(email)
    return cleaned

Rubric (1 pt each): deleted all four pure-narration comments (empty list, loop, get, append, return); identified rows[1:] as a header-skip why; identified strip/lower as a normalization why; identified the loose "@" check as a deliberate-decision why; result has fewer comments than the original, each load-bearing.

B2 — solution. The why behind dropping the first and last after sorting is that it's a trimmed mean — discarding the lowest and highest values to reduce the influence of outliers. The one comment the function needs:

def score(values):
    # Trimmed mean: drop the lowest and highest before averaging so a single
    # outlier reading can't skew the score.
    values = sorted(values)
    return sum(values[1:-1]) / len(values[1:-1])

The narration (# sort, # remove, # average) all goes; the statistical intent — invisible in the mechanics — is the one thing worth saying.

B3 — solution (model docstring).

def split_name(full_name):
    """Split a full name into its first and last components.

    Middle names are ignored; the first whitespace-separated token is
    treated as the first name and the last token as the last name.

    Args:
        full_name: The full name to split (e.g. "Ada Lovelace").
            Leading/trailing whitespace is ignored.

    Returns:
        A (first, last) tuple of strings.

    Raises:
        ValueError: If `full_name` contains fewer than two tokens.
    """

Rubric: one-line summary present; notes the middle-name behavior (a real gotcha a caller needs); Args describes the parameter and its whitespace handling; Returns names the tuple shape; Raises states the failure condition. Caller can use it without reading the body.

B4 — solution (model).

/**
 * Truncate a string to a maximum length, appending an ellipsis if cut.
 *
 * @param {string} text - The string to truncate.
 * @param {number} [maxLength=80] - Maximum length of the result,
 *   including the ellipsis. Defaults to 80.
 * @returns {string} The original string if within the limit, otherwise
 *   the string cut to fit with a trailing "…".
 * @throws {TypeError} If `text` is not a string.
 */

Rubric: @param for both args with {type}; [maxLength=80] marks optional + default; @returns describes both branches; @throws states the TypeError.

B5 — solution.

celsius = read_sensor()
fahrenheit = celsius * 9 / 5 + 32
if fahrenheit > MAX_SAFE_TEMP_F:
    trigger_alarm()

All three comments are deleted because their causes are designed out: t/f become celsius/fahrenheit (the conversion is now self-evident), and the magic 104 becomes MAX_SAFE_TEMP_F. Rubric: two renames + one named constant; zero comments remaining; the what is now self-documenting.

B6 — solution.

def parse(text: str, base: datetime | None = None) -> datetime: ...
def average(numbers: list[float]) -> float: ...
def find_user(user_id: int, include_inactive: bool = False) -> User | None: ...

(Assumptions to state: average takes floats and returns a float; find_user returns User | None because a missing user is plausible. Accept reasonable variants — e.g. Sequence[float], str user IDs — as long as the assumption is stated.)

B7 — solution.

def apply_discount(price, code):
    discount = 0.0
    if code == "SAVE10":
        discount = 0.10
    elif code == "SAVE20":
        discount = 0.25
    # Round to 2 decimals: currency can't carry fractional cents, and the
    # billing system expects standard half-up rounding here. Bug #91.
    return round(price * (1 - discount), 2)

Three fixes: deleted every narration comment; fixed the stale lie (the comment said "SAVE20 gives 20 percent" but the code applies 0.25 — correct the comment to match the code, or flag the discrepancy to a human if you can't tell which is right — here we removed the false comment and let the named codes speak); kept the one real why (the rounding rationale + bug link). Rubric: narration gone; the lie caught and resolved; the genuine why preserved.

C1 — rubric. Two genuinely different whys for the same sleep(0.25). Good answers: (a) "Rate-limit: the API allows 4 requests/sec, so space calls at least 250ms apart." (b) "Give the worker thread a moment to register before we poll it; without the delay the first poll races ahead of registration and returns a spurious 'not found.' See #173." The point you should feel: the line is identical; the documentation is completely different because the intent differs — which is exactly why the why can't be read off the code.

C2 — rubric (model below).

def words_at_least(sentence: str, min_length: int) -> list[str]:
    """Return the words in a sentence that are at least `min_length` long.

    Args:
        sentence: The text to split on whitespace.
        min_length: The minimum word length to keep (inclusive).

    Returns:
        A list of the qualifying words, in their original order.
    """
    # Length filter is inclusive (>=) so min_length=3 keeps 3-letter words.
    return [word for word in sentence.split() if len(word) >= min_length]

Rubric (1 pt each): renamed fwords_at_least, ssentence, nmin_length, wword, r→(eliminated via comprehension or results); docstring with summary/args/returns; type hints on signature; one why-comment (the inclusive >= is a defensible candidate, or a note on word definition); assumptions stated.

C4 — rubric. A real function, three genuine re-derived whys, each converted to a placed comment. Grade yourself: were the three things you listed actually whys (intent/constraint/decision) and not whats? If you listed "what the function does," re-do it — you want the questions that cost you time, which are always whys.

D1 — solution sketch. The logic breaks on the internal vs. external what. "Comment the why not the what" governs internal comments — narration inside the body that restates lines a reader can already see. A docstring's parameter descriptions are external: they state the contract for a caller who is not reading the body, so for them the "what" of each parameter is exactly the information they need and can't get elsewhere. Deleting it doesn't remove redundancy; it removes the contract. (Same boundary as §24.4: type hints document the what of the data on purpose, because the caller needs it.) The rule is about redundant whats inside the implementation, not the necessary whats of an interface.

D2 — solution sketch. No contradiction once you split what from why. The whats that should vanish into the code are the self-evident ones a good name or structure makes obvious (# increment i, # d is days until expiry) — §24.5 designs those comments away. The whys that must stay are the reasons no name can hold (the 4.7s gateway quirk, the header-row skip, the list-over-set decision) — §24.2 keeps those. "Make the code self-documenting" and "comment the why" are the same strategy split by target: retire the what-comments so that the why-comments can do their job without being buried.

D3 — rubric. (a) one or two lines, the staleness reason; (b) a caller-facing note about possible 90s staleness, in the docstring's terms, without the upstream-implementation detail (the caller doesn't need it); (c) the full decision with the rejected alternatives (why not 60, why not 300). The one-sentence reflection should name audience (Ch 2) and register/level of detail (Ch 7) as what changed: the implementer's comment carries the mechanism; the caller's docstring carries the consequence; the ADR carries the full reasoning and alternatives.

D4 — solution sketch. Module A is far better documented despite zero comments: it sits near the calibrated center for what-documentation (names, constants, types, docstrings all carry it, none can drift). Module B is the over-documented failure dressed as diligence: comment-on-every-line buries signal, single-letter names force the comments to exist, mixed docstring styles are elegant-variation clutter, and no type hints means nothing is checked. What A might still be missing: the whys — no naming, however good, can record why a value is what it is or why a non-obvious approach was chosen. A could be perfectly self-documenting on what and still silent on the one thing only a comment can hold.

D5 — rubric. A tight, cost-led paragraph (Ch 20 move) that: (1) leads with the dollar/hour cost of re-derivation, not the principle; (2) names future-self and the next maintainer as the guaranteed readers; (3) concedes honestly that what-comments and over-documentation waste time and cause drift — which builds credibility; (4) lands on the precise ask: capture the whys, skip the whats. If your paragraph led with "documentation is important," rewrite it to lead with the cost — you're persuading a skeptic, not stating a value.

M1 — solution. The comment needs both: it's narrating the what (# ... we check if it is not none restates if x is not None) and it's bloated Chapter-3 fog ("in order to be able to," "whether or not"). Best fix: delete it — the code says it. The prose needs the Chapter 3 cut: "It is important to note that," "in most cases," "will generally" are all fog → "The function returns a list of results." (And verify it's true — if it sometimes returns something else, that's the real content.) The lesson: a comment problem and a prose-clarity problem can look similar; the comment's primary sin is redundancy (delete), the prose's is bloat (cut).

M2 — solution (model).

def parse(text, base=None):
    """Parse a date expression into a datetime.

    Relative expressions resolve against `base`.

    Args:
        text: The date expression to parse.
        base: The reference time relative expressions resolve against.

    Returns:
        A datetime for the parsed expression.

    Raises:
        ValueError: If the input cannot be parsed.
    """

Rubric: summary first (topic sentence); the scattered facts reordered to the natural given-new sequence (what it does → params → return → raises); proper PEP 257 form. The original jumbled "Raises... It parses... base... returns... relative" into a sequence a reader could follow.

M3 — solution. It violates one term per concept / no elegant variation (Ch 7). It's worse in code than in prose because a varied name in code can actively mislead a reader into thinking user, account, and member are different entities (a correctness risk, not just a style blemish), and because the name is read at every use, the confusion compounds. It's a naming problem, not a comment problem — the fix is to pick one canonical term and rename, not to add a comment explaining that they're the same (a comment papering over a fixable name is itself an antipattern, §24.5).

M5 — rubric. Feedback that (Ch 12): opens by naming something genuine that's good (the insightful why-comment — quote it, praise it specifically); then gives specific, actionable notes on the three what-comments (delete, with the reason), the magic number (name it), and the missing docstring (add the contract); critiques the code/comments, never the person; and is concise and kind. A jerk version ("your comments are useless") gets zero; the skill is specificity + warmth + keeping the good.

M6 — solution. Chapter 6 fix (it's broken English): "this loop it iterate... incrementing each of the counter" → grammatical would be "Iterate over the counters, incrementing each." Chapter 24 fix (it's the wrong comment entirely): that grammatical version still just narrates the what — so the real fix is to delete it, because for c in counters: c.increment() says exactly that already. The Chapter 24 fix mattered more: fixing the grammar of a comment that shouldn't exist is polishing something you should throw away. Always ask whether the comment should exist before you fix how it reads.

E-series — self-assess against the chapter: did your critique/convention distinguish what from why throughout? Did you favor coupled (checked) documentation over decoupled prose? Did your style guide model the brevity it demands (E3)?