The failure signatures of generated code
Seven shapes account for most bugs in code you did not write. Learn to name them on sight, then repair a function that swallows its own errors.
Bugs come in shapes
Reviewing gets fast when you stop looking for "a bug" and start looking for known shapes. Code written quickly by anyone, human or model, fails in a small number of recognisable ways. Seven of them cover most of what you will actually find.
1. Off by one at a boundary
The correction that is right in the common case and wrong at the edge.
def page_count(total, per_page):
return total // per_page + 1 # wrong on every exact multiple
Signature: an arithmetic + 1 or - 1, a slice bound, or range(1, n) versus range(n). Test the exact multiple, the first element, and the last element.
2. Unhandled empty or None
The function assumes there is at least one of something, or that a value is present.
def busiest_hour(counts):
return counts.index(max(counts)) # ValueError on []
Signature: max, min, [0], [-1], or division by len(...) with no guard above it. Test [], and test data with a None in it.
3. Silently swallowed errors
The one that costs the most in production, because it produces no signal at all.
for row in rows:
try:
values[row.split("=")[0]] = int(row.split("=")[1])
except Exception:
pass # the bad rows just vanish
Signature: except Exception: pass, except: continue, or a try block wrapping more lines than the one that can actually fail. A dropped row is indistinguishable from a row that was never there, so the report is quietly short and nobody finds out for a month.
4. The wrong side of a comparison
> where the policy said "at or above", <= where it said "strictly under". No crash, no clue, and the difference shows up only on the exact boundary value.
def sla_breached(hours_open, sla_hours):
return hours_open > sla_hours # policy says "at or above", so equality is missed
Signature: any comparison against a threshold, a limit, a quota, or an expiry. Always test the exact threshold value.
5. The ignored return value
Python's string and tuple methods return new objects. Calling one and throwing away the result is a no-op that looks like work.
def redact(text, secrets):
for secret in secrets:
text.replace(secret, "[REDACTED]") # result discarded, text unchanged
return text
Signature: a bare method call on its own line whose object is immutable. str.replace, str.strip, str.upper, sorted, and list.copy all return; list.sort, list.append, and dict.update all mutate. Mixing the two conventions up is a top-three cause of "it does nothing and does not error".
6. The API that does not exist
Confident calls to methods, keyword arguments, or modules that were never in the library. str.rreplace, sorted(x, cmp=...), dict.get_or_default. These at least fail loudly, unless someone wrapped them in a try.
7. The complexity you did not ask for
Correct output, wrong cost. A membership test against a list inside a loop, list.pop(0) in a queue, string concatenation in a loop. It passes every test on ten rows and falls over on a hundred thousand.
Sorting a bug into its shape
Naming the shape tells you the test to write. That is the whole payoff.
Each line comes from a function you are reviewing. Sort each one by how it will fail.
Errors are data, not noise
The repair for a swallowed error is almost never "let it crash". It is to make the failure visible in the value the function returns, so the caller can decide.
def load_thresholds(rows):
values = {}
rejected = []
for row in rows:
try:
name, raw = row.split("=")
values[name] = int(raw)
except ValueError:
rejected.append(row) # the row is reported, not deleted
return {"values": values, "rejected": rejected}
Two changes carry all the weight. The except names ValueError rather than Exception, so a typo in your own code inside that block still crashes the way it should. And the bad row leaves the function in the return value, so a caller can count it, log it, or refuse to start.
Interview nuance: when you are asked to review code out loud, naming the shape is the move. "This except swallows everything, so a malformed row is indistinguishable from a missing row" is a specific, testable claim. "This error handling could be better" is not, and interviewers hear the difference immediately.
def redact(text, secrets):
for secret in secrets:
text.replace(secret, "[REDACTED]") # result discarded
return text
print(redact("token abc123", ["abc123"])) # token abc123, unchangedApply
Your turn
The task this lesson builds to.
Repair load_thresholds(rows) in the starter so that a row it cannot parse is reported
instead of dropped.
The contract: each row is a string of the form "name=value" where value is an integer. Return
a dictionary with "values" (the parsed name to integer mapping, in the order the rows arrived)
and "rejected" (the raw rows that did not parse, in the order they arrived).
The generated version swallows every failure with except Exception: pass, so a malformed row is
indistinguishable from a row that was never sent. Narrow the exception it catches and put the bad
rows in the return value.
3 hints and 4 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Your team pipes support-ticket text into an analytics tool, and a redact function is
supposed to strip API keys out first. An assistant wrote the version in the starter. Nobody has
reported a problem, which is itself worrying, so you are checking it against a set of recorded
tickets before the next release.
Write a function first_leaking_case(cases) that returns the index of the first case in which
redact disagrees with its contract, or -1 if it handles all of them.
Each case is a two-element list [text, secrets]. The contract: redact(text, secrets) returns
text with every string in secrets replaced by "[REDACTED]". Note that a case whose secret
never appears in the text is handled correctly, so it is not a leak. Keep first_leaking_case as
the last function in the file.
3 hints and 4 automated checks are waiting in the workspace.