Choosing edge cases
20 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Most wrong answers come from inputs you did not think of. This lesson gives a short checklist of awkward inputs and shows how to read a problem statement to find the borders.
The usual suspects
An edge case is an input at the border of what a program accepts, such as an empty list or the largest allowed size, where mistakes are most likely.
Most bugs hide at the border of what a program accepts. Before you trust a solution, try a short list of awkward inputs: empty, one item, two items, duplicates, negatives, zeros and the largest size.
cases = { "empty": [], "one item": [7], "duplicates": [4, 4, 4], "negatives": [-3, -8], "already sorted": [1, 2, 3], "reversed": [3, 2, 1],}for name, data in cases.items(): print(name, max(data, default=None))Example 1 · The middle of a list
def middle(items): return items[len(items) // 2] if items else None
for n in range(0, 5): data = list(range(n)) print(n, middle(data))0 None 1 0 2 1 3 1 4 2
What this code does
- Lengths 0 to 4 show the empty case, one item, and even and odd lengths.
- For an even length,
len // 2picks the second of the two middle items.
A dictionary of named cases keeps the list in one place. max(data, default=None) handles the empty list, which plain max(data) would reject with an error.
Read the statement for borders
Every limit in a problem gives two cases: the smallest and the largest allowed value. If n is from 1 to 100000, test n = 1 and n = 100000. If values can be negative, test a negative one.
def average(marks): if not marks: return 0.0 return sum(marks) / len(marks)
print(average([]))print(average([80]))print(average([-5, 5]))Example 2 · Average of an empty list
def average(marks): if not marks: return None return sum(marks) / len(marks)
for data in ([], [80], [60, 90]): print(data, average(data))[] None [80] 80.0 [60, 90] 75.0
What this code does
- Three cases in one loop: empty, one item and two items.
- Without the guard the empty case would stop the program with a ZeroDivisionError.
The guard for an empty list stops a division by zero. Decide what the empty answer should be from the statement, then write the guard.
Cases that break common code
Some inputs break common ideas. A list with equal values breaks code that expects every value to be different. A list with two items breaks code that looks at the middle one.
def second_largest(values): ordered = sorted(set(values), reverse=True) return ordered[1] if len(ordered) > 1 else None
print(second_largest([5, 5, 3]))print(second_largest([9, 9]))Example 3 · Duplicates break sorted()[-2]
values = [5, 5, 3]naive = sorted(values)[-2]safe = sorted(set(values))[-2]print(naive, safe)5 3
What this code does
- The naive version returns 5, the largest value again, because the two fives sit side by side.
- Removing duplicates first gives the true second largest value, 3.
set removes the duplicates first, so [5, 5, 3] gives 3. Equal values leave nothing second, so the answer is None.
Build the largest case in code
You cannot type 100000 numbers by hand. Make them with a loop or with a seeded random generator, so every run builds the same list and you can compare answers.
import random
random.seed(7)big = [random.randint(-10**9, 10**9) for _ in range(100000)]print(len(big), min(big) <= max(big))Example 4 · A seeded largest case
import random
random.seed(11)n = 200000nums = [random.randint(-1000, 1000) for _ in range(n)]print(n, sum(nums), min(nums), max(nums))200000 -120771 -1000 1000
What this code does
- The seed makes every run give the same numbers, so the printed sum can be compared.
- The range includes negatives, so the minimum is below zero.
random.seed(7) fixes the sequence. The list has the maximum size, and Python integers do not overflow, so the real risk is speed, not size.
Next step
Check your understanding
11 quick questions