Spotting the missing edge case
Code that works in the demo and dies in production almost always died on an edge case — an input the author never tested because it never came up at their desk. The reviewer's job is to ask the question the author forgot: *what about the inputs nobody tried?*
The usual suspects, worth running through on every function that takes data:
- Empty —
[],"",{}. Does a loop still work? Does a division have anything to divide? - None — a value that was supposed to be there but isn't. Passing
Nonetosum(),len(), or.strip()raises. - Zero — the classic
ZeroDivisionError, but also "zero items", "zero balance", "0% discount". - One — a single-element collection often reveals an off-by-one a longer list hides.
- Negative — negative amounts, counts, indices. Did the author assume positive?
The fix is a guard clause: a short check at the top that handles the awkward input and returns early, so the rest of the function can assume clean data. if not numbers: return 0.0 covers both None and [] in one line, because both are falsy.
Syntax
# Guard-clause pattern — handle the edge first, then proceed safely:
def something(data):
if not data: # catches None, [], "", {}, 0
return <safe default>
# ... from here on, data is non-empty and real
# Edge-case roll-call to run on any input-taking function:
# empty · None · zero · single element · negativeWorked examples
The classic: division by len() on empty input (REV-102)
# BUGGY — crashes on an empty list:
def average(numbers):
return sum(numbers) / len(numbers)
# average([]) -> ZeroDivisionError: division by zero
# average(None) -> TypeError: 'NoneType' is not iterable
# FIXED — one guard handles BOTH empty and None:
def average(numbers):
if not numbers:
return 0.0
return sum(numbers) / len(numbers)
print(average([2, 4, 6])) # 4.0
print(average([])) # 0.0
print(average(None)) # 0.0The silent edge case: no match found
# Runs without error, but watch the 'none found' case:
def first_even(nums):
for n in nums:
if n % 2 == 0:
return n
return None # explicit: nothing matched
print(first_even([1, 3, 4])) # 4
print(first_even([1, 3, 5])) # None <-- caller MUST handle this
# A reviewer asks: does the caller check for None,
# or will it crash later doing `first_even(x) + 1`?Single element exposes a slicing assumption
# BUGGY — assumes there's always a 'rest' after the first:
def describe(items):
head = items[0] # IndexError on []
rest = items[1:]
return f"{head} plus {len(rest)} more"
# FIXED — guard the empty case explicitly:
def describe(items):
if not items:
return "nothing"
return f"{items[0]} plus {len(items) - 1} more"
print(describe(["a"])) # a plus 0 more
print(describe([])) # nothingCommon mistakes & gotchas
Confusing 'handled' with 'crashes cleanly'
A function that raises a clear ValueError on bad input may be fine — that's a deliberate contract. The bug is the *unintended* crash (a ZeroDivisionError the author never considered) or the silent wrong answer. Decide which inputs are errors and which need a real default.
0 and "" are falsy too
if not x: fires on 0, 0.0, "", [], {} AND None. Usually that's what you want, but if 0 is a legitimate value you must distinguish, test if x is None: explicitly instead — or you'll guard away a real input.
Edge cases the author 'knows' can't happen
"The list is never empty here" is the most expensive sentence in code review. Inputs you control today become inputs a stranger controls tomorrow. If the function is reachable from outside, guard it.
Why it matters
The happy path is the part that already works — nobody ships code that fails the demo. Real systems break at the boundaries: the empty cart, the user with no orders, the first run before any data exists. A reviewer who instinctively probes empty/None/zero catches the failures that would otherwise surface at 2am in production.