Reading tracebacks, naming what went wrong, and fixing bugs that don't even throw an error — the highest-value skill in this whole course.
An exception is an object Python creates the moment something goes wrong — it stops your program right there and prints a report explaining what happened, unless you catch it.
nums = [1, 2, 3]
print(nums[5])
# IndexError: list index out of range
Run the example above yourself. Confirm the exact error.
IndexError: list index out of rangeTry {"a": 1}["b"]. What exception, and what does it say?
KeyError: 'b'Try None + "text". What exception?
TypeErrorTry int("abc"). What exception?
ValueErrorTry printing a variable name you never created. What exception?
NameErrorOpen problems/01_what_is_an_exception.py and do all 5 problems locally.
A traceback is the call chain that led to a crash. Read it from the bottom — that line tells you what actually broke. The lines above it, going up, show the chain of calls that got you there.
def fetch_active_users_broken(users):
active = []
for u in users:
if u["status"] == "ACTIVE":
active.append(u)
# forgot return!
def count_by_dept(users):
counts = {}
for u in users:
counts[u["profile"]["department"]] = 1
return counts
active = fetch_active_users_broken(USERS)
result = count_by_dept(active)
# TypeError: 'NoneType' object is not iterable
You met this exact bug in Module 7. Here's its name: the bottom line (TypeError) is where it crashed. The real cause — the missing return — is two functions away.
Run the example above. Confirm the exact error message.
TypeError: 'NoneType' object is not iterableWhich line of the traceback tells you the exception type — the top or the bottom?
In the example, which function call happens first — fetch_active_users_broken or count_by_dept?
fetch_active_users_brokenDoes the traceback show you WHERE the code broke, or WHY it's wrong? (one word: where/why)
Write your own two-function chain with a deliberate crash in the second function. Run it, read your own traceback bottom-up.
Open problems/02_reading_tracebacks.py and do all 5 problems locally.
The crash site is the line the traceback points at — where Python finally gave up. The cause site is where the real mistake was made. They're often not the same line, and sometimes not even the same function.
# Cause site: this function forgot to return
def fetch_active_users_broken(users):
active = []
for u in users:
if u["status"] == "ACTIVE":
active.append(u)
# no return -- THIS is the cause
# Crash site: the error actually shows up here
result = count_by_dept(fetch_active_users_broken(USERS))
# TypeError -- but the real mistake was one function earlier
In the example above, name the cause site (which function, which missing line).
fetch_active_users_broken, missing return activeName the crash site.
count_by_dept, the for u in users: lineCould the crash site and cause site ever be the exact same line? Give a one-line example where they are.
print(1/0) — cause and crash are the same lineWhy is "fix the crash site" often the wrong move? (one sentence, as a comment)
Take your two-function chain from the last sub-skill's Lab 5. Identify its cause site and crash site out loud (as comments above each line).
Open problems/03_crash_vs_cause.py and do all 5 problems locally.
Same idea, applied to the real fixture — a helper function used inside a loop, crashing partway through.
def get_department(user):
return user["profile"]["department"]
def show_all_departments(users):
for u in users:
print(get_department(u))
show_all_departments(USERS)
Run the code above. How many departments print before it crashes, and what's the exception?
KeyError: 'department'Cause site: the fixture — Layla (USERS[7]) has no department key. Crash site: inside get_department, called from inside show_all_departments's loop — two functions removed from where the "bad" data actually lives.
Confirm it yourself — run it, count the printed lines before the crash.
7Name the cause site in one sentence.
Name the crash site in one sentence.
Write a guarded version of get_department using .get() with a default — confirm it runs all 12 without crashing.
Open problems/04_multifunction_crash.py and do all 4 problems locally.
Every bug so far has crashed — loud, obvious, a traceback to read. There's a worse category: a script that runs perfectly, prints something, and the something is wrong. No error. Nothing tells you to look.
def most_logins(counts):
top = None
for name in counts:
top = name # never actually compares -- just keeps overwriting
return top
print(most_logins({"a": 5, "b": 10, "c": 3}))
# "c" -- but "b" has the highest count (10). This is silently wrong.
Run the example. Confirm it prints "c", not the true max.
cManually check: which key actually has the highest value in {"a": 5, "b": 10, "c": 3}?
bWhy is this bug more dangerous than a crash? One sentence.
Fix most_logins so it actually compares and tracks the true max. Test it on the same dict.
bWrite a two-function chain (fetch + analyze) where the analyze step has this exact "silently overwrite instead of compare" bug on real USERS data (e.g. finding the department with the most users). Run it, notice the wrong answer.
Open problems/05_silent_bugs.py — 5 problems, #5 is a checkpoint combining sub-skills 1–4.
try / excepttry runs code that might crash. If it does, except catches the exception instead of letting the whole program die — you decide what happens next.
try:
print(1 / 0)
except:
print("something went wrong")
Run the example above. Confirm it prints the fallback message, not a crash.
something went wrongWrap int("abc") in a try/except that prints "couldn't convert".
couldn't convertWrap {"a": 1}["b"] the same way, printing "key not found".
key not foundPut code that does NOT crash inside a try. Confirm the except block never runs.
Wrap the fixture's USERS[7]["profile"]["department"] access in try/except, printing "Unknown" on failure.
UnknownOpen problems/06_try_except.py and do all 5 problems locally.
Instead of catching everything blindly, name the exact exception you expect — except KeyError:, except TypeError:. That way an unrelated bug doesn't get silently swallowed too.
try:
print({"a": 1}["b"])
except KeyError:
print("that key doesn't exist")
Run the example. Confirm the specific message.
that key doesn't existCatch int("xyz") with except ValueError:, printing "not a number".
not a numberCatch [1, 2][5] with except IndexError:, printing "out of range".
out of rangeWrite try/except KeyError: around code that actually raises TypeError — predict, then run. What happens?
Catch None + "x" with except TypeError:, printing "can't combine those".
can't combine thoseOpen problems/07_specific_exceptions.py and do all 5 problems locally.
raiseraise deliberately triggers an exception — useful when your own code detects bad data and wants to stop loudly, on purpose, instead of continuing with garbage.
def set_age(age):
if age < 0:
raise ValueError("age can't be negative")
print(f"age set to {age}")
set_age(-5) # ValueError: age can't be negative
Run the example. Confirm the exact error.
ValueError: age can't be negativeCall set_age(30) instead — confirm no crash.
age set to 30Write a function set_status(status) that raises ValueError if status isn't "ACTIVE", "SUSPENDED", or "DEPROVISIONED". Test it with an invalid value.
Wrap a call to that function in try/except ValueError:, catching your own raised error.
Write a function that raises KeyError on purpose (not from a real dict access — just raise KeyError("custom message")).
KeyError: 'custom message'Open problems/08_raise.py and do all 5 problems locally.
try/except isn't always the right tool. If a bug is your mistake (like the missing-return bug), the fix is fixing your code — not wrapping it in a try/except that hides the real problem. Catch exceptions that come from things outside your control: missing/messy data, not your own logic errors.
Is a missing return statement something to catch with try/except, or something to fix directly? (one word)
Is a user record missing a department key (real messy data) something to catch, or fix in your code? (one word)
.get())Wrap a call to a function with a real logic bug (like the accumulator KeyError from Module 7) in a blind try/except: pass. What's wrong with doing this instead of fixing the bug?
Write one sentence (as a comment) explaining when try/except is the RIGHT tool.
Open problems/09_judgment_call.py and do all 4 problems locally.
A real script, several bugs, not told how many. Diagnose before you fix.
def get_active(users):
active = []
for u in users:
if u["status"] == "ACTIVE":
active.append(u)
# bug?
def count_by_dept(users):
counts = {}
for u in users:
counts[u["profile"]["department"]] = counts[u["profile"]["department"]] + 1
return counts
active = get_active(USERS)
result = count_by_dept(active)
How many bugs are in this script, and what crashes first?
TypeError: 'NoneType' object is not iterableThere are actually three bugs: get_active is missing its return, count_by_dept uses counts[dept] + 1 (would KeyError on first occurrence), and it also uses direct ["department"] access (would crash on Layla). But only the FIRST one — the missing return — actually triggers, because the script never gets far enough to hit the other two. Fixing bugs one at a time often reveals the next one underneath.
Fix the missing return. Run again — what crashes now?
KeyError, first department it seesFix the accumulator to use .get(dept, 0) + 1. Run again — what crashes now?
KeyError: 'department' on LaylaFix the direct key access to use .get("department", "Unknown"). Run the whole script — confirm it finishes clean.
Print the final counts dict from your fully-fixed version.
{'IT': 2, 'Security': 2, 'Engineering': 3, 'Unknown': 1}Explain in three sentences what was wrong and why fixing it one bug at a time was necessary here — three separate crash sites, one at a time.
Open problems/10_drill1_three_bugs.py — 5 problems, #5 is a checkpoint combining sub-skills 1–9.
A subtler bug — appending the wrong thing to a list, not a missing return or a bad key.
def collect_group_names(user):
names = []
for g in user["groups"]:
names.append(g) # bug: appends the whole group dict, not g["name"]
return names
def show_shouted(names):
for n in names:
print(n.upper())
names = collect_group_names(USERS[0])
show_shouted(names)
What happens when this runs?
AttributeError: 'dict' object has no attribute 'upper'Cause site: collect_group_names appended the whole group dict instead of group["name"]. Crash site: a completely different function, show_shouted, which assumed it was getting strings. The crash message doesn't mention "wrong type appended" anywhere — you have to trace it back yourself.
Fix collect_group_names to append g["name"] instead. Run the whole thing again.
Print the fixed result for USERS[0] (Julio).
ENGINEERING then VPN-USERSName the cause site and crash site in one sentence each.
Why didn't collect_group_names itself crash? (it ran fine, appending dicts just fine)
.upper() is calledOpen problems/11_drill2_wrong_type.py and do all 4 problems locally.
One of the dangerous ones — no crash. Removing items from a list while looping over it skips elements, because positions shift out from under the loop.
def remove_inactive(users):
for u in users:
if u["status"] != "ACTIVE":
users.remove(u)
return users
test = [
{"id": "a", "status": "ACTIVE"},
{"id": "b", "status": "SUSPENDED"},
{"id": "c", "status": "SUSPENDED"},
{"id": "d", "status": "ACTIVE"},
]
print(remove_inactive(test))
Both "b" and "c" are SUSPENDED. Does the result remove both?
"c" stays in the list even though it should have been removedNo error anywhere. When "b" gets removed, everything after it shifts down one position — the loop's next step then skips over "c" entirely. This is why you never mutate a list you're currently looping over.
Run the example. Confirm "c" really does survive.
"c"Fix it: build a NEW list of only the ACTIVE users instead of removing from the original. Test it on the same data.
"a" and "d"Run your fixed version on the full USERS fixture. Print the count.
8In one sentence: why does removing from a list while looping over it cause elements to get skipped?
Open problems/12_drill3_mutate_while_iterating.py and do all 4 problems locally.
The same "never tracks a max" bug from earlier, now with a second problem: what happens when the input is empty.
def most_logins(counts):
for name in counts:
top = name
return top
print(most_logins({"a": 5, "b": 10, "c": 3}))
print(most_logins({}))
The first call is silently wrong (you already know this). What happens on the SECOND call, with an empty dict?
UnboundLocalError: cannot access local variable 'top' where it is not associated with a valueA genuinely new exception type. Since counts is empty, the loop body never runs even once — top never gets created at all, so return top fails. The bug that was silent for real data becomes a hard crash for edge-case data.
Run the example. Confirm both outcomes yourself.
Fix most_logins so it actually tracks the true max AND handles an empty dict safely (return None for empty). Test both cases.
b then NoneRun your fixed version on real login counts built from LOGIN_COUNTS. Print the top email.
mfoster@example.com (203, the highest)What's the general lesson: why did testing only with real, non-empty data hide this bug for so long?
Open problems/13_drill4_silent_and_crash.py and do all 4 problems locally.
Combine everything: guard patterns (.get(), in checks from earlier modules) for data you expect to be messy, and try/except/raise for genuinely unexpected failures.
Write a function that safely gets a user's department using .get() — no exception handling needed, since .get() already prevents the crash.
Write a function that wraps a genuinely risky operation (like int() on unpredictable text) in try/except ValueError, returning None on failure.
Combine both patterns in one small chain: fetch users (guard missing department), then convert a login count string safely (try/except), then report.
Run your combined chain over the real fixture, confirming no crash across all 12 records.
Open problems/14_defensive_scripts.py and do all 4 problems locally.
Everything from this module, over the whole fixture: crashes, crash-far-from-cause, silent skips, silent wrong answers, and defensive handling.
Loop all USERS, safely printing each department with a guard — no crash on Layla.
Build a new list (not mutate-in-place) of only SUSPENDED users. Print the count.
2Find the department with the most ACTIVE users, using a properly-fixed max-tracking function (not the silent-overwrite version).
Engineering (3)Wrap a risky conversion (e.g. converting a login count to a different type) in try/except, handling failure gracefully.
One script combining: a guarded department report, a properly-built (not mutated) filtered list, a fixed max-finder, and at least one try/except — over the full 12-record fixture, zero crashes, zero silent wrong answers.
Open problems/15_full_diagnostic_report.py — 5 problems, #5 is the final checkpoint for all of Module 8.
Run python check.py in problems/ to grade everything.