Chapter 11 · Reviewer

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 None to sum(), 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  ·  negative

Worked 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.0

The 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([]))           # nothing

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