Now make it green
The starter below is the module plus your full five-test suite from part 1, still reporting 2/5 passed. Fix the module, not the tests, they encode the spec. Work like the professional this course built:
- pick ONE failing test
- hypothesize the cause (the exception bestiary and boundary analysis have already done most of the thinking)
- change one thing in the module, rerun, watch that test flip green
- repeat until
5/5 passed
If a fix turns something else red, the safety net from lesson 1-2 just earned its keep. All three bugs are single-line fixes.
Three one-line fixes
The module with all three bugs corrected, and the tests untouched, since they encode the spec.
def new_wallet(): return {"balance": 0, "history": []} def deposit(w, amount): if amount <= 0: raise ValueError("amount must be positive") w["balance"] += amount w["history"].append(("deposit", amount)) def withdraw(w, amount): if amount <= 0: raise ValueError("amount must be positive") if amount > w["balance"]: raise ValueError("insufficient funds") w["balance"] -= amount w["history"].append(("withdraw", amount)) def test_deposit_adds_to_balance(): w = new_wallet() deposit(w, 50) assert w["balance"] == 50 def test_deposit_zero_rejected(): w = new_wallet() try: deposit(w, 0) assert False except ValueError: pass def test_withdraw_reduces_balance(): w = new_wallet() deposit(w, 50) withdraw(w, 20) assert w["balance"] == 30 def test_withdraw_exact_balance_allowed(): w = new_wallet() deposit(w, 50) withdraw(w, 50) assert w["balance"] == 0 def test_history_records_withdrawals(): w = new_wallet() deposit(w, 50) withdraw(w, 20) assert w["history"][-1] == ("withdraw", 20) def run_tests(tests): passed = 0 for test in tests: try: test() print("PASS", test.__name__) passed += 1 except AssertionError: print("FAIL", test.__name__) except Exception as e: print(f"FAIL {test.__name__} (crashed: {type(e).__name__})") print(f"{passed}/{len(tests)} passed") run_tests([test_deposit_adds_to_balance, test_deposit_zero_rejected, test_withdraw_reduces_balance, test_withdraw_exact_balance_allowed, test_history_records_withdrawals])
Output
PASS test_deposit_adds_to_balance PASS test_deposit_zero_rejected PASS test_withdraw_reduces_balance PASS test_withdraw_exact_balance_allowed PASS test_history_records_withdrawals 5/5 passed
Bug 1 is deposit's guard, which must reject zero too, so amount < 0 becomes amount <= 0. Bug 2 is withdraw's funds check, which should reject only amounts strictly greater than the balance, so >= becomes >. Bug 3 is the history label, where withdraw appended ("deposit", amount) as a copy-paste leftover and now records ("withdraw", amount).
Fixing them one at a time and rerunning after each is what the loop asks for, and the reason is the lesson 7-1 rule about changing one thing per experiment. Three simultaneous edits that produce 5/5 leave you unsure whether all three were needed, and a mistake in one is hard to attribute.
Note that withdraw's first guard was already correct at amount <= 0, sitting two lines above a wrong one. Inconsistency between neighboring comparisons is the smell from lesson 3-2, and here it is the clue that one of the two was written carelessly.
Bug 3 is the only one that is not a boundary error, and it is the kind that boundary analysis would never find. It took a test that asserted on the recorded history, which is the checklist thinking of asking what else a function promises besides its return value.
What the tests do for the next engineer
Six months later, someone refactoring wallet to store cents as integers gets one specific thing from your five tests: they define the spec executably, so if the refactor keeps all five green, the contract still holds.
This closes the circle back to lesson 1-2. Tests outlive the bugs they caught and become the refactoring safety net, so the next engineer changes the internals freely and trusts the green report, exactly as you did with count_vowels on day one.
Notice what the tests do not constrain. Nothing in them mentions floats, so switching the internal representation to integer cents is invisible to the suite as long as the balance still reads 0 after depositing 50 and withdrawing 50. That is the mark of tests written against behavior rather than implementation.
The history tests are the most valuable ones for a refactor of this kind, because the tuple format is a promise other code depends on. Without them, a refactor could reasonably decide to store history as dictionaries, and every consumer of w["history"] would break somewhere far from the change.
Naming the thing you are guarding against
Rerunning all five tests after fixing withdraw, rather than only the one that was red, is checking for a regression, a change that breaks previously working behavior.
The term was defined back in lesson 1-2, and it is why the whole suite runs on every change and not just the test you were chasing. A fix is a change like any other, so it can break a neighbor.
The risk here is concrete rather than theoretical. Loosening >= to > in withdraw widens what the function accepts, and if some other behavior had quietly depended on the stricter check, the test covering it goes red immediately instead of six months later.
Rerunning the whole suite after every fix is the cheapest regression insurance there is, costing milliseconds in this module and, in a real project, the CI run from lesson 6-3 that happens whether you remember or not.
With that habit you have completed the loop this course set out to build: test, trust green, change fearlessly, and debug with a method.
Where this goes next
The habits are the point, not the toy runner.
In real projects you will write these same tests with pytest, the standard Python test tool. Every test_ function you wrote here works there almost unchanged, assert and all, and pytest supplies the reporting, the filtering, and the table-driven conveniences you built by hand in lessons 1-3 and 3-3.
Your suite will run automatically in continuous integration, from lesson 6-3, on every change you propose, guarding teammates you will never meet.
The debugging loop travels just as well. Reproduce, minimize, hypothesize, bisect, and prove the fix with a test is identical whether the code is ten lines or ten million, because none of those steps depends on the size of the program.
What changes with scale is only the cost of each step, which is why the pyramid from lesson 6-1 and the logarithm from lesson 9-1 matter more the bigger the codebase gets.
Test first, trust green, change fearlessly.