Skip to main content

The failure signatures of generated code

Level 5: Level 5: Judging Code You Did Not Writemedium24 mincode reviewdebuggingerror handlingverification

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.

Check yourself
A config loader wraps its parse loop in try/except Exception: pass. Ten of a thousand rows are malformed. What does the system do?

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

Check yourself
With redact above, what is the result of redact('token abc123', ['abc123'])?

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.

Check yourself

Each line comes from a function you are reviewing. Sort each one by how it will fail.

name.strip()
return sum(values) / len(values)
if item in seen_list:
return balance > limit
config = json.loads(response)
output += line + chr(10)

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.

Check yourself
You are reviewing a generated function and you spot except Exception: pass around a parse loop. What is the strongest single change to ask for?
Worked example (Python)
def redact(text, secrets):
    for secret in secrets:
        text.replace(secret, "[REDACTED]")   # result discarded
    return text


print(redact("token abc123", ["abc123"]))   # token abc123, unchanged

Apply

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.