Chapter 11 · Reviewer

Writing good review feedback

Finding problems is half the job. The other half is communicating them so they actually get fixed — and that starts with triage. "This corrupts the customer's total" and "I'd rename this variable" are not the same severity, and treating them the same buries the one that matters under the one that doesn't.

Every finding gets a category:

  • bug — wrong behaviour, data corruption, a crash. This must change.
  • security — an exploitable or unsafe pattern (eval, injection, leaked secret). This must change, often urgently.
  • style — works correctly but is hard to read: poor names, tangled structure, repetition.
  • nit — trivial and optional: a stray comment, spacing, a one-word preference.

The categories drive blocking vs non-blocking. Bugs and security findings *block* the merge — the PR shouldn't ship until they're addressed. Style is a conversation. Nits are explicitly optional — prefix them so the author knows they're free to ignore: "nit: ...".

What makes feedback land:

  • Specific — point at the line and say what's wrong, not "this feels off".
  • Actionable — say what to do, or ask a real question. "Consider a guard for the empty list" beats "handle edge cases".
  • Kind — review the code, not the coder. "This branch returns high for a below-range value" — never "why would you write this?".

The reviewer mindset: you and the author are on the same side, shipping good code. You're a second pair of eyes, not a gatekeeper scoring points. The best review makes the author better *and* keeps the bug out of production.

Syntax

# Severity ladder — classify every finding, then decide blocking:
#
#   bug       wrong behaviour / crash / data loss     --> BLOCKING
#   security  exploitable / unsafe / leaked secret     --> BLOCKING
#   style     correct but hard to read                 --> discuss
#   nit       trivial / cosmetic / optional            --> non-blocking
#
# A structured verdict the way a reviewer files it:
findings = {
    1: "bug",        # adds the discount instead of subtracting
    2: "security",   # hardcoded live API key
    3: "style",      # variable named `x`
    4: "nit",        # redundant restating comment
}

Worked examples

Classifying four planted issues (REV-105)

# CODE UNDER REVIEW (judge it, don't run it):
#   API_KEY = "sk-live-9f83hsd_REAL_SECRET"        # ISSUE 2
#   def apply_discount(price, pct):
#       x = price + (price * pct / 100)             # ISSUE 1 (math) + ISSUE 3 (name)
#       return x  # return the discounted price     # ISSUE 4

# The reviewer's verdict — each issue ranked by what it actually costs:
findings = {
    1: "bug",        # ADDS the discount, returns more than full price
    2: "security",   # a live key committed in source
    3: "style",      # `x` says nothing about what it holds
    4: "nit",        # the comment just restates the obvious
}
print(findings[1], findings[2], findings[3], findings[4])
# bug security style nit

Why issue 1 is a bug, not a style nit — trace it

# The planted math error actually changes the answer:
def apply_discount_buggy(price, pct):
    return price + (price * pct / 100)   # + should be -

def apply_discount(price, pct):
    return price - (price * pct / 100)

print(apply_discount_buggy(100, 20))   # 120.0  <-- charges MORE for a discount
print(apply_discount(100, 20))         # 80.0   <-- correct
# Wrong returned value = bug = blocking. Not a matter of taste.

Two security findings outrank everything else (REV-BOSS)

# A PR that hardcodes a credential AND feeds raw input into a query:
#   DB_PASSWORD = "hunter2_prod"   # committed secret
#   user_id = request.args["id"]   # TODO: validate  (flows into a query)

# Both are security — both block the merge:
verdict = {
    "password": "security",   # a hardcoded prod credential
    "todo": "security",       # unvalidated input into a query = injection risk
}
print(verdict)
# {'password': 'security', 'todo': 'security'}
# A 'TODO: validate' on input that reaches a query is not a nit — it's a hole.

Common mistakes & gotchas

Drowning the real bug in nits

Twelve comments about spacing and one about a data-corrupting bug, all the same weight, and the author fixes the spacing and misses the bug. Lead with what blocks; mark cosmetics as 'nit:' so the signal isn't lost in the noise.

Mistaking a style preference for a bug

Blocking a merge over a variable name or a formatting choice burns goodwill and slows the team for no behavioural gain. If the code is correct, your structural feedback is a suggestion, not a gate. Be honest about which of your comments actually matter.

Vague or unkind feedback that can't be acted on

"This is wrong" or "this is messy" gives the author nothing to do and something to resent. Point at the line, state the concrete problem, and propose a direction. Review the code, never the person — the goal is better code, not a scoreboard.

Why it matters

A review is only as good as the action it triggers. Ranking findings by severity is what turns a wall of comments into a clear decision: fix these two before merge, consider these, ignore the rest. Specific, kind, well-triaged feedback is the difference between a reviewer the team trusts and one they learn to skim past — and it's the skill that scales an engineer into a senior.