Challenge Goals
3 min- Spot edge cases that the "happy path" hides.
- Write a failing test that proves each bug exists.
- Fix the code so all tests pass.
- Build the habit: a bug isn't fixed until a test guards it.
Warm-Up · The Rules
5 minFor each broken function: (1) find the bug by thinking about edge cases, (2) write a pytest test that fails on the current code, (3) fix the code so your test passes. Order matters — the failing test first proves you actually found the bug.
This is "regression-test-driven bug fixing": write a test that reproduces the bug (it fails), then fix the code (it passes). The test now permanently guards against that bug returning. It's how professionals close bug tickets.
The Five Buggy Functions
14 min# bugs.py — all five LOOK fine. Each has one subtle bug.
# 1. Average — works on [10, 20, 30]...
def average(nums):
return sum(nums) / len(nums)
# 2. Find the max — works on [3, 1, 2]...
def my_max(nums):
biggest = 0
for n in nums:
if n > biggest:
biggest = n
return biggest
# 3. Is this a palindrome? — works on "racecar"...
def is_palindrome(s):
return s == s[::-1]
# 4. Count vowels — works on "hello"...
def count_vowels(text):
count = 0
for ch in text:
if ch in "aeiou":
count += 1
return count
# 5. Apply discount — works on price 200...
def discounted(price, percent):
return price - price * percent / 100Hints (don't peek unless stuck)
1. what if the list is empty? 2. what if ALL numbers are negative? e.g. [-5, -2, -8] 3. should "RaceCar" be a palindrome? what about "race car"? 4. what about UPPERCASE vowels? "HELLO"? 5. what if percent is 150? or negative? should price ever go below 0?
Worked Example · Cracking Bug #2
12 minTake my_max. The happy path passes:
assert my_max([3, 1, 2]) == 3 # ✅ passes
Now think edge cases — all negatives:
def test_my_max_all_negative(): assert my_max([-5, -2, -8]) == -2 # ❌ FAILS: returns 0!
AssertionError: assert 0 == -2
Bug found: biggest = 0 assumes a non-negative max. With all-negative input, no value beats 0, so it wrongly returns 0.
The fix — start from the first element, not 0:
def my_max(nums): if not nums: raise ValueError("empty sequence") biggest = nums[0] # start from a real value for n in nums[1:]: if n > biggest: biggest = n return biggest
# now the test passes — and guards forever def test_my_max_all_negative(): assert my_max([-5, -2, -8]) == -2 # ✅ def test_my_max_empty_raises(): with pytest.raises(ValueError): my_max([]) # bonus edge case caught
Read the diff
The failing test came first — it proved the bug was real, not imagined. Then the fix made it green. Now if anyone re-introduces biggest = 0, the test screams. That's the complete bug-fix cycle: reproduce → fix → guard.
Crack the Other Four
13 minWrite a failing test for the empty-list case, then fix average (decide: return 0, or raise?).
Test is_palindrome("RaceCar") and count_vowels("HELLO"). Both ignore uppercase. Fix with .lower().
Test discounted(100, 150) — it returns -50! Decide the rule (clamp percent to 0-100? clamp result to ≥ 0?) and write tests for the boundaries before fixing.
Hint
def discounted(price, percent): if not 0 <= percent <= 100: raise ValueError("percent must be 0-100") return round(price - price * percent / 100, 2)
Mini-Challenge · Full Suite + Fixes
8 minProduce a complete test_bugs.py with at least 2 tests per function (a happy path + the bug-exposing edge case), plus the fixed bugs.py. Every test green on the fixed code; every test red on the original.
Show the full fixed file
# bugs.py — fixed def average(nums): return sum(nums) / len(nums) if nums else 0 def my_max(nums): if not nums: raise ValueError("empty") m = nums[0] for n in nums[1:]: if n > m: m = n return m def is_palindrome(s): s = "".join(c.lower() for c in s if c.isalnum()) return s == s[::-1] def count_vowels(text): return sum(1 for ch in text.lower() if ch in "aeiou") def discounted(price, percent): if not 0 <= percent <= 100: raise ValueError("percent 0-100") return round(price - price * percent / 100, 2)
Non-negotiables: a failing-on-original test per bug, all green on the fix. Note is_palindrome now also handles spaces/punctuation via isalnum — a real improvement the tests drove.
Recap
3 minAll five bugs hid on edge cases the happy path skips: empty input, all-negatives, uppercase, out-of-range percent. The professional cycle is reproduce (failing test) → fix → guard (the test stays). Edge-case thinking from Lesson 4 plus pytest from Lesson 11+ is the whole toolkit. Next: mocking, for when the thing you test depends on something slow or external.
Homework
4 minFind (or write) 3 functions of your own that have subtle edge-case bugs. For each: write the bug-exposing test first, confirm it fails, fix the code, confirm it passes. Submit test_*.py + the fixed code, plus a one-line note of each bug.
Use the fixed bugs.py as a model for the cycle. The key discipline: the test must fail on the broken code first, or you haven't proven you found the bug.