Comments — Practice Tasks¶
Twelve "fix the comments" exercises. Every comment you keep should pay rent: it must say something the code cannot. Each task gives you a scenario, a snippet whose comments are pulling their weight badly, and an instruction. Work the fix in your head (or your editor) before opening the solution. Languages rotate between Go, Java, and Python on purpose — comment smells look the same in every syntax.
Table of Contents¶
- Task 1 — Delete the redundant comment by improving the name (Java)
- Task 2 — Remove commented-out code (Python)
- Task 3 — Replace a journal/attribution comment with VCS (Go)
- Task 4 — Delete the closing-brace comment by extracting a function (Java)
- Task 5 — Convert a WHAT comment into a WHY comment (Go)
- Task 6 — Fix the misleading, outdated comment that contradicts the code (Python)
- Task 7 — Write a proper doc-comment for a public API (Go)
- Task 8 — Turn an opaque comment into a clarifying one with a link (Java)
- Task 9 — Replace a section-banner comment with extracted functions (Python)
- Task 10 — Promote intent: the comment apologizes for the code (Java)
- Task 11 — Encode an invariant the comment merely asserts (Go)
- Task 12 — Comment audit (Python — open-ended)
How to Use¶
- Read the snippet and decide what each comment is for before you touch it. Most comments fall into one of three buckets: redundant (says what the code already says), misleading (says something the code does not do), or load-bearing (says something the code genuinely cannot — a reason, a warning, a link).
- Try the fix yourself, then expand the
<details>block. Compare your reasoning to the solution's, not just your diff. - The default move in this chapter is delete or replace, not "write a better comment." A comment you can erase by renaming a variable or extracting a function is a comment that was never worth maintaining.
- Difficulty climbs from Easy (mechanical deletions) to Hard (audits and invariants encoded in types). Do them in order the first time.
The decision you are practicing on every task:
Task 1 — Delete the redundant comment by improving the name (Java)¶
Difficulty: Easy
Scenario: A teammate sprinkled "helpful" comments above lines that already explain themselves. Each comment is a second copy of the code in English — and a second thing to keep in sync.
public class CartPricing {
public BigDecimal total(List<Item> items) {
// the running total
BigDecimal t = BigDecimal.ZERO;
// loop over every item
for (Item i : items) {
// multiply price by quantity
BigDecimal lt = i.getPrice().multiply(BigDecimal.valueOf(i.getQty()));
// add to the running total
t = t.add(lt);
}
// return the total
return t;
}
}
Instruction: Remove every comment. The code must remain at least as readable — achieve that by improving the names, not by adding new comments.
Solution
**Reasoning.** Every comment here was *redundant* — it restated the line below it. Redundant comments are worse than no comment because they double the maintenance surface: rename `t` and the comment "the running total" silently survives, and now you have two sources of truth. The fix is to fold the comment's information into the name. `t` → `runningTotal`, `lt` → `lineTotal`, `i` → `item`. The loop and the `multiply`/`add` calls are self-evident to any reader who knows the language; commenting them insults the reader and rots on the next edit. Rule of thumb: *if a comment translates code into English one-for-one, delete it and let the names do the translating.*Task 2 — Remove commented-out code (Python)¶
Difficulty: Easy
Scenario: During a refactor someone "kept the old version just in case." Three months later nobody knows whether the dead block is the safe fallback or a known-bad attempt.
def normalize_phone(raw: str) -> str:
# old implementation — keep for reference
# digits = "".join(c for c in raw if c.isdigit())
# if len(digits) == 10:
# return "+1" + digits
# return digits
digits = "".join(ch for ch in raw if ch.isdigit())
if len(digits) == 10:
return "+1" + digits
if len(digits) == 11 and digits.startswith("1"):
return "+" + digits
raise ValueError(f"cannot normalize phone: {raw!r}")
Instruction: Decide what to do with the commented-out block and apply it. Justify the call.
Solution
**Reasoning.** Delete it. Commented-out code is the single most reliable source of distrust in a codebase. A reader cannot tell whether it is (a) a rollback path you must keep, (b) a half-finished idea, or (c) abandoned cruft — so they leave it, and it accretes. The "keep for reference" justification is exactly what version control exists to satisfy: the old implementation is one `git log -p` or `git blame` away, permanently, with the commit message and date that explain *why* it changed. Source files are for the code that runs. If you truly need to resurrect the old logic, you will recover it from history with full context, not from a stale comment that has been silently diverging from the live version (note the live version already grew an 11-digit branch the dead copy never got).Task 3 — Replace a journal/attribution comment with VCS (Go)¶
Difficulty: Easy
Scenario: This file has grown a changelog at the top and an "added by" tag on a function. The code is being maintained by hand-rolling what git already records perfectly.
// Package invoicing computes invoice totals.
//
// Changelog:
// 2023-04-01 Alice: initial version
// 2023-06-12 Bob: fixed rounding bug (issue #214)
// 2024-01-09 Alice: added EU VAT handling
// 2024-03-22 Carol: refactored discount logic
package invoicing
// added by Bob, 2023-06-12
// reviewed by Alice
func applyRounding(amount Cents) Cents {
return (amount + 50) / 100 * 100
}
Instruction: Remove the journal and attribution comments. Keep anything that is still genuinely useful, and say where the deleted information now lives.
Solution
**Reasoning.** Two distinct smells, one fix: *git*. - **Journal comment** (the changelog). Every line of it is reproduced — more accurately — by `git log`, which records the exact commit, diff, author, timestamp, and message. A hand-maintained changelog is always behind reality because nobody updates it on the small commits, so it becomes a misleading partial history. Delete it; the package doc-comment should describe *what the package is*, not its biography. - **Attribution comment** ("added by Bob / reviewed by Alice"). Ownership and review history live in `git blame` and the pull-request record, which cannot lie or fall out of date. An inline author tag creates fake ownership: when Carol edits Bob's function, does she update the tag? She won't, and now it is wrong. What I *added* is the only thing git could not supply: a doc-comment that explains the rounding rule the expression encodes, so the reader does not have to reverse-engineer `(amount + 50) / 100 * 100`. That is the WHY/intent layer — see [Task 5](#task-5--convert-a-what-comment-into-a-why-comment-go).Task 4 — Delete the closing-brace comment by extracting a function (Java)¶
Difficulty: Medium
Scenario: The nesting got deep enough that someone annotated each closing brace so they could tell which block was ending. The comments are a symptom; the disease is depth.
public void process(List<Order> orders) {
for (Order order : orders) {
if (order.isValid()) {
for (LineItem item : order.getItems()) {
if (item.isInStock()) {
if (item.getQty() > 0) {
reserve(item);
} // end if qty > 0
} // end if in stock
} // end for items
} // end if valid
} // end for orders
}
Instruction: Get rid of the // end ... comments. Do not do it by simply deleting them — remove the reason they were written.
Solution
public void process(List<Order> orders) {
orders.stream()
.filter(Order::isValid)
.forEach(this::reserveAvailableItems);
}
private void reserveAvailableItems(Order order) {
for (LineItem item : order.getItems()) {
if (isReservable(item)) {
reserve(item);
}
}
}
private boolean isReservable(LineItem item) {
return item.isInStock() && item.getQty() > 0;
}
Task 5 — Convert a WHAT comment into a WHY comment (Go)¶
Difficulty: Medium
Scenario: The comment describes the mechanics the reader can already see, while the genuinely surprising part — why this exact value — goes unexplained. Six months later someone will "clean up" the magic number and break production.
func retryDelay(attempt int) time.Duration {
// multiply 100ms by 2 to the power of attempt, capped at 30 seconds
backoff := 100 * time.Millisecond * (1 << attempt)
if backoff > 30*time.Second {
backoff = 30 * time.Second
}
return backoff
}
Instruction: Rewrite the comment so it explains something the code does not already say. Delete what is redundant.
Solution
func retryDelay(attempt int) time.Duration {
const (
base = 100 * time.Millisecond
// The upstream payment gateway closes idle connections at 30s,
// so capping here keeps us inside one connection lifecycle and
// avoids a reconnect storm under sustained failure (see INC-4821).
cap = 30 * time.Second
)
backoff := base * (1 << attempt)
if backoff > cap {
backoff = cap
}
return backoff
}
Task 6 — Fix the misleading, outdated comment that contradicts the code (Python)¶
Difficulty: Medium
Scenario: The function was changed but its comment was not. The comment now actively lies, which is worse than no comment: a reader who trusts it will make a wrong decision.
def is_eligible(user) -> bool:
# Returns True only for users over 18 with a verified email.
if user.age < 21:
return False
if not user.email_verified:
return False
if user.country in BLOCKED_COUNTRIES:
return False
return True
Instruction: The comment and the code disagree on two points (the age threshold and the country check). Resolve the contradiction. State your assumption about which one is correct, and make the comment and code agree.
Solution
Assume the **code** is the source of truth (it is what actually shipped and what tests exercise), and that the age bump to 21 and the country block were deliberate. Then the comment must be corrected — or, better, removed because good names plus a docstring make it unnecessary: **Reasoning.** An outdated comment is a *misleading* comment, and misleading comments are the most expensive kind: they cost a reader their trust in *every* comment in the file. Here the comment claimed "over 18" while the code requires 21, and omitted the country block entirely — a reader reviewing a bug report about a blocked user would be sent down the wrong path. The discipline is twofold: (1) when code and comment conflict, treat the **code as truth** unless you have evidence the change was a mistake, then fix the comment in the *same* commit; (2) prefer to eliminate the comment's burden — naming the threshold `MIN_AGE` and pointing the docstring at the authoritative policy document means the inline prose can no longer drift away from the logic, because there is barely any prose left to drift. A comment you do not have to maintain cannot become a lie.Task 7 — Write a proper doc-comment for a public API (Go)¶
Difficulty: Medium
Scenario: This is an exported function in a library other teams import. It has no doc-comment, so consumers must read its body to learn how to call it — including the parts they cannot see, like the error contract and the concurrency guarantee.
func ParseConfig(r io.Reader) (*Config, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("read config: %w", err)
}
var c Config
if err := yaml.Unmarshal(data, &c); err != nil {
return nil, fmt.Errorf("parse config: %w", err)
}
if c.Port == 0 {
c.Port = 8080
}
return &c, nil
}
Instruction: Write a doc-comment that follows Go conventions and tells a caller everything they need to use this without reading the body: what it does, the input contract, the defaults it applies, the error behavior, and any safety notes.
Solution
// ParseConfig reads YAML configuration from r and returns the parsed Config.
//
// It reads r to EOF; the caller retains ownership of r and is responsible for
// closing it. Any field omitted in the YAML keeps its zero value, except Port,
// which defaults to 8080 when unset or zero.
//
// ParseConfig returns an error if r cannot be read or if the bytes are not
// valid YAML for the Config schema; the returned error wraps the underlying
// cause and can be inspected with errors.Is / errors.As. On error the returned
// *Config is nil.
//
// ParseConfig performs no I/O beyond reading r and is safe for concurrent use.
func ParseConfig(r io.Reader) (*Config, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("read config: %w", err)
}
var c Config
if err := yaml.Unmarshal(data, &c); err != nil {
return nil, fmt.Errorf("parse config: %w", err)
}
if c.Port == 0 {
c.Port = 8080
}
return &c, nil
}
Task 8 — Turn an opaque comment into a clarifying one with a link (Java)¶
Difficulty: Hard
Scenario: The code does something that looks wrong — a deliberate, non-obvious workaround. The existing comment gestures at it without giving the reader anything actionable, so every future maintainer re-investigates from scratch.
Instruction: Replace this useless comment with a clarifying comment that actually equips the next reader: explain why the odd dance is necessary and link to the authoritative source.
Solution
// Work around OpenJDK-8261236: ArrayList.addAll on an empty list with a
// presized backing array throws under -XX:+UseParallelGC on JDK 11.0.2.
// Seeding one element forces the slow-path copy, which is unaffected; we then
// drop the sentinel. Remove once we drop JDK < 11.0.4.
// Repro + analysis: https://bugs.openjdk.org/browse/JDK-8261236
list.add(null); // sentinel: forces ArrayList onto the safe copy path
list.addAll(results);
list.remove(0); // drop the sentinel
Task 9 — Replace a section-banner comment with extracted functions (Python)¶
Difficulty: Hard
Scenario: A long function is organized into phases marked by banner comments. The banners are honest — they name real phases — but a comment that labels a block is usually a function asking to be born.
def onboard_employee(emp, hr_system, payroll, it):
# ----- validate -----
if not emp.email:
raise ValueError("email required")
if emp.start_date < date.today():
raise ValueError("start date in the past")
# ----- create accounts -----
account = it.create_account(emp.email)
it.add_to_groups(account, emp.department.default_groups)
mailbox = it.provision_mailbox(emp.email)
# ----- enroll in payroll -----
record = payroll.create_record(emp.ssn, emp.salary)
payroll.set_tax_withholding(record, emp.tax_form)
# ----- notify -----
hr_system.send_welcome(emp.email)
hr_system.schedule_orientation(emp, emp.start_date)
return account
Instruction: Each banner marks a conceptual step. Refactor so the banners become unnecessary, and the top-level function reads as a sequence of named steps.
Solution
def onboard_employee(emp, hr_system, payroll, it):
validate(emp)
account = create_accounts(emp, it)
enroll_in_payroll(emp, payroll)
notify(emp, hr_system)
return account
def validate(emp):
if not emp.email:
raise ValueError("email required")
if emp.start_date < date.today():
raise ValueError("start date in the past")
def create_accounts(emp, it):
account = it.create_account(emp.email)
it.add_to_groups(account, emp.department.default_groups)
it.provision_mailbox(emp.email)
return account
def enroll_in_payroll(emp, payroll):
record = payroll.create_record(emp.ssn, emp.salary)
payroll.set_tax_withholding(record, emp.tax_form)
def notify(emp, hr_system):
hr_system.send_welcome(emp.email)
hr_system.schedule_orientation(emp, emp.start_date)
Task 10 — Promote intent: the comment apologizes for the code (Java)¶
Difficulty: Hard
Scenario: The comment is doing emotional labor — apologizing, hedging, warning — instead of the code being clear enough not to need apology. Some of these comments hint at real risk; the others should evaporate once the code is fixed.
public Money convert(Money amount, Currency to) {
// sorry this is ugly, rates is a global, careful
double rate = Rates.GLOBAL.get(amount.currency() + "_" + to);
// this might be null sometimes, not sure when
double converted = amount.value() * rate;
// TODO: handle the case where rate is missing (it crashes)
return new Money(converted, to);
}
Instruction: Three comments, three different problems. Eliminate each by fixing the code rather than improving the prose. Keep a comment only where one is genuinely warranted.
Solution
public Money convert(Money amount, Currency to) {
Rate rate = rates.lookup(amount.currency(), to)
.orElseThrow(() -> new UnknownRateException(amount.currency(), to));
return amount.times(rate.factor()).withCurrency(to);
}
Task 11 — Encode an invariant the comment merely asserts (Go)¶
Difficulty: Hard
Scenario: A comment states a precondition the caller must honor. Comments cannot enforce anything; the compiler and the type system can. Where the rule can be made unrepresentable, a comment is the weakest possible guardrail.
type Percentage struct {
// Value must always be between 0 and 100. Callers must check before setting.
Value float64
}
func ApplyDiscount(price Cents, p Percentage) Cents {
// assumes p.Value is in [0, 100] — undefined behavior otherwise
return price - Cents(float64(price)*p.Value/100)
}
Instruction: The comments describe an invariant ("must be between 0 and 100"). Move the invariant from prose into the construction of the type so that an out-of-range Percentage cannot exist. Keep a comment only for what the types still cannot express.
Solution
// Percentage is a value in the closed range [0, 100]. The zero value (0%) is
// valid; construct other values with NewPercentage, which is the only way to
// obtain one outside this package — guaranteeing every Percentage is in range.
type Percentage struct {
value float64 // unexported: cannot be set out of range from outside
}
func NewPercentage(v float64) (Percentage, error) {
if v < 0 || v > 100 {
return Percentage{}, fmt.Errorf("percentage out of range [0,100]: %v", v)
}
return Percentage{value: v}, nil
}
func (p Percentage) Fraction() float64 { return p.value / 100 }
func ApplyDiscount(price Cents, p Percentage) Cents {
// No range check needed: a Percentage cannot exist outside [0, 100].
return price - Cents(float64(price)*p.Fraction())
}
Task 12 — Comment audit (Python — open-ended)¶
Difficulty: Hard
Scenario: Below is a realistic snippet carrying every comment smell from this chapter at once. List each comment, classify it, and write a one-line fix. This is the exercise that proves you can see the smells in the wild.
# CacheManager.py
# created 2022-08-01 by Dana
# modified 2023-05-17 by Eli: added TTL
# modified 2024-02-03 by Dana: thread safety (?)
import threading, time
class CacheManager:
def __init__(self, ttl):
self.ttl = ttl # the ttl
self.store = {} # the store
self.lock = threading.Lock()
# self.max_size = 1000 # commented out, may re-add later
def get(self, key):
# acquire the lock
with self.lock:
# check if key in store
if key in self.store:
value, ts = self.store[key]
# if not expired, return it
if time.time() - ts < self.ttl:
return value
return None
def set(self, key, value):
# store the value with a timestamp
with self.lock:
self.store[key] = (value, time.time())
# NOTE: this never evicts, the dict grows forever. don't worry about it for now
# end set
Instruction: Produce a table: comment → category → fix. Then state the order in which you'd attack them.
Solution
| Comment | Category | Fix | |---|---|---| | `# created ... by Dana` / `# modified ... by Eli` / `# modified ... by Dana` | Journal + attribution | Delete all three; `git log`/`git blame` hold this accurately. | | `# CacheManager.py` (filename as comment) | Redundant | Delete; the file already has a name. | | `# the ttl`, `# the store` | Redundant | Delete; `self.ttl` and `self.store` already say it. | | `# self.max_size = 1000 ...` | Commented-out code | Delete; recover from history if `max_size` is ever wanted. | | `# acquire the lock` | Redundant | Delete; `with self.lock:` is self-evident. | | `# check if key in store` | Redundant | Delete; `if key in self.store:` says exactly that. | | `# if not expired, return it` | Redundant-ish (WHAT) | Delete after extracting an `_is_fresh(ts)` helper whose name carries the meaning. | | `# store the value with a timestamp` | Redundant | Delete; the line states it. | | `# NOTE: this never evicts ... don't worry about it for now` | Apology + hidden defect | Don't comment a known bug — fix it (add eviction / `max_size`) or file a tracked issue with a link; the "don't worry" tone is an unowned TODO. | | `# modified ... thread safety (?)` (the `(?)`) | Misleading / uncertain | The author isn't sure the locking is correct — resolve it with a test, then make the doc-comment state the actual concurrency guarantee. | | `# end set` | Closing-brace comment | Delete; Python has no braces and the `def` boundary is unambiguous regardless. | Cleaned result:import threading
import time
class CacheManager:
"""A thread-safe in-memory cache with per-entry TTL.
NOTE: entries are evicted lazily on read; there is no size bound yet.
Tracked in CACHE-112.
"""
def __init__(self, ttl: float):
self.ttl = ttl
self.store: dict = {}
self.lock = threading.Lock()
def get(self, key):
with self.lock:
entry = self.store.get(key)
if entry and self._is_fresh(entry):
return entry[0]
return None
def set(self, key, value):
with self.lock:
self.store[key] = (value, time.time())
def _is_fresh(self, entry) -> bool:
return time.time() - entry[1] < self.ttl
Self-Assessment¶
Rate yourself honestly. You have internalized this chapter when you can answer yes to all of these:
- I delete a comment by improving a name or extracting a function before I consider rewriting it.
- I never leave commented-out code in a commit — I trust git to remember.
- I do not write journal or attribution comments; I let version control own history and ownership.
- When I see a WHAT comment, I either delete it (the code already says it) or upgrade it to a WHY.
- I treat a comment that contradicts the code as a priority bug, and I fix code and comment in the same commit.
- I write real doc-comments for public APIs covering contract, defaults, errors, and concurrency.
- I replace opaque "hack, don't remove" notes with a cause, a mechanism, an exit condition, and a link.
- I recognize banner and closing-brace comments as functions waiting to be extracted.
- I move invariants from prose into types/constructors whenever the language lets me.
- I keep exactly the comments that say something the code cannot — and no others.
Scoring: 9–10 yes → you have the chapter. 6–8 → re-do Tasks 5, 8, and 11. Below 6 → re-read the chapter README and start over from Task 1.
Related Topics¶
- junior.md — the beginner-level walkthrough of each comment anti-pattern.
- find-bug.md — snippets where a comment hides (or causes) a real bug.
- optimize.md — turning mediocre comments into load-bearing ones.
- Chapter README — the positive rules: when a comment does earn its place.
- Refactoring: code smells — Extract Function and Rename underlie most fixes here; the comment smells are the same smells seen from the documentation side.
In this topic