S011 · Errors, Exceptions & Debugging with pdb
Failures are data — learn to read them.
Module M01: Python Foundations · Session 11 of 130 · Track: SW 🐛
What you'll be able to do after this session
- Explain Errors, Exceptions & Debugging with pdb to a friend in one minute, out loud, no notes.
- Recognise it when you see it in real code / systems / papers.
- Complete the hands-on exercise at the end without looking up help.
Prerequisites
If you can't answer these in 30 seconds each, redo the linked session first:
Pre-read (skim before the session)
- 🎥 Intuition (5–15 min) · Python Exception Handling in 8 Minutes — Tech With Tim's crisp mental model of try/except.
- 🎥 Deep dive (30–60 min) · Corey Schafer — Errors and Exceptions — the canonical long-form walkthrough that every Python dev cites.
- 🎥 Hands-on demo (10–20 min) · Python pdb — Interactive Debugging — live pdb session, break/step/next/print.
- 📖 Canonical article · Python docs — Errors and Exceptions — the source of truth; skim §8.1–8.6.
- 📖 Book chapter / tutorial · Real Python — Python Exceptions: An Introduction — best beginner tutorial online.
- 📖 Engineering blog · Sentry — Python Error Monitoring Best Practices — how production teams actually catch and log errors.
(a) Intuition · 5 min
The one-paragraph mental model. Why this concept exists, what problem it solves, and the analogy that makes it stick.
In one sentence: Failures are data — learn to read them.
Imagine you're following a recipe and the recipe says "add 2 cups of flour" but the flour bag is empty. You have two choices: panic and stop cooking forever, or notice the problem, walk to the store, and come back. Exceptions are Python's way of saying "something went wrong here" — a traceback is just the trail of breadcrumbs showing which function called which, all the way down to the line that broke.
Every Python error is a signal, not a scolding. KeyError: 'user_id' doesn't mean you're stupid — it means a dictionary didn't have the key you asked for. TypeError: unsupported operand type(s) for +: 'int' and 'str' is Python saying "you asked me to add 5 and 'hello' — pick one." The whole point of exceptions is that they carry structured information you can act on: the type, the message, the stack.
The forever-gotcha: Never write except: or except Exception: without a plan. Silent catches are how bugs live in production for six months. Catch the specific exception you expect, log everything else.
(b) Visual walkthrough · 15 min
Diagrams, worked examples, step-by-step visuals.
When an exception is raised, Python unwinds the call stack looking for a matching except. If none is found, the interpreter prints the traceback and exits.
Worked example 1 — reading a traceback bottom-up:
Traceback (most recent call last):
File "app.py", line 42, in <module>
process(order)
File "app.py", line 25, in process
total = compute_total(order["items"])
File "app.py", line 12, in compute_total
return sum(i["price"] for i in items)
KeyError: 'price'
Read from the bottom: the actual error is KeyError: 'price' on line 12. The lines above show how you got there: main → process → compute_total. The fix is not in main — it's that one of your items lacks a price field. Add a print or a pdb.set_trace() on line 12 to see which item.
Worked example 2 — the try/except/else/finally family:
| Clause | Runs when | Typical use |
|---|---|---|
try | always — the guarded block | the risky call |
except | a matching exception was raised | recovery / logging |
else | try finished with no exception | code that depends on try success |
finally | always, even if exception uncaught / return | cleanup (close file, release lock) |
try:
f = open("data.csv")
rows = f.read()
except FileNotFoundError:
rows = "" # sensible default
else:
print(f"loaded {len(rows)} bytes") # only if open succeeded
finally:
if "f" in dir(): f.close() # alwayspdb cheat-sheet (drop import pdb; pdb.set_trace() or breakpoint() anywhere):
| Command | Meaning | Command | Meaning |
|---|---|---|---|
n | next line | p x | print value of x |
s | step into call | l | list source around here |
c | continue to next bp | w | where — show stack |
q | quit | pp x | pretty-print x |
(c) Hands-on · 20 min
Code you type and run. Not pseudo-code — real, runnable, copy-pasteable.
Save as debug_demo.py and run python debug_demo.py.
"""Practice reading tracebacks and using breakpoint()."""
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
log = logging.getLogger(__name__)
class OrderError(Exception):
"""Domain-specific error so callers can catch this vs generic ones."""
def compute_total(items: list[dict]) -> float:
total = 0.0
for i, item in enumerate(items):
# breakpoint() # <-- uncomment to drop into pdb here
try:
total += item["price"] * item["qty"]
except KeyError as e:
raise OrderError(f"item {i} missing field {e}") from e
return total
def process(order: dict) -> float:
try:
total = compute_total(order["items"])
except OrderError:
log.exception("bad order %s — using 0", order.get("id"))
return 0.0
else:
log.info("order %s total=%.2f", order["id"], total)
return total
finally:
log.info("done with order %s", order.get("id"))
if __name__ == "__main__":
good = {"id": "A1", "items": [{"price": 10, "qty": 2}, {"price": 5, "qty": 1}]}
bad = {"id": "B2", "items": [{"price": 10, "qty": 2}, {"qty": 1}]} # missing price
process(good)
process(bad)Observe when you run:
goodorder printsorder A1 total=25.00.badorder logs anERROR bad order B2with a full traceback including theraise ... from echain (seeThe above exception was the direct cause…).finallyruns for both orders — you'll seedone with ordertwice.- The process exits code 0 — we caught
OrderError, we didn't crash. - Uncomment
breakpoint()and rerun: at the prompt typep item,p total,n,c.
Try this modification: change except OrderError to except KeyError and rerun. You'll see the uncaught OrderError propagate out of process, main crashes, exit code 1. This proves exception-type specificity matters.
(d) Production reality · 10 min
How this shows up in real systems, gotchas, war stories, common bugs.
The single most common production Python bug is a bare except: hiding a real crash. A 2021 Uber postmortem traced an 8-hour billing outage to except Exception: pass around a Redis call — the Redis was down, every write silently failed, and the metric graph looked healthy because no exceptions were logged. Rule: never catch without either re-raising, logging with log.exception(...) (which auto-includes the traceback), or converting to a domain-specific error.
Second: losing the cause chain. In Python 3, always use raise NewError("context") from original — the from preserves the original traceback under "The above exception was the direct cause…" Without it, on-call engineers see your wrapper error and have no idea what actually failed.
Third: pdb in production is a footgun. Never ship breakpoint() — it will hang the process on the first request. Instead, top teams use:
- Sentry / Rollbar / Datadog Error Tracking for automatic exception capture with request context, user id, breadcrumbs.
- Structured logging (
loggingwith JSON formatter, orstructlog) so tracebacks are queryable in Kusto / Splunk / Elasticsearch. faulthandlerfor segfault-style crashes in native extensions.py-spy/pyspy dump --pid Nto attach to a live stuck Python process without restarting it — the modern replacement for "SSH in and run pdb".
Netflix's Python services log every exception with a trace id so you can join across a request that crossed 12 microservices. At Microsoft, the OneNote Copilot pipelines wrap external LLM calls in a retry decorator that catches only httpx.TimeoutException and openai.RateLimitError — anything else is a bug and should crash loudly so we notice.
Gotcha: except Exception does not catch KeyboardInterrupt or SystemExit (both inherit from BaseException) — that's on purpose so Ctrl-C always works. If you write except BaseException you'll trap Ctrl-C and make your program un-killable.
(e) Quiz + exercise · 10 min
- In a traceback, which line is the actual error — the top or the bottom?
- What is the difference between
except Exceptionandexcept BaseException, and why does it matter for Ctrl-C? - When would you use
elsein a try/except/else/finally block instead of putting the code insidetry? - What does
raise NewError("msg") from edo thatraise NewError("msg")does not? - Name three pdb commands and what they do.
Stretch (connects to S007 Functions): write a decorator @retry(times=3, on=(TimeoutError,)) that retries a wrapped function up to N times only on the listed exception types, logs each attempt, and re-raises the last exception with the full chain preserved. Test it against a function that fails twice then succeeds.
Explain-out-loud test
If you can't teach these 3 things to a friend without notes, redo the session:
- What is Errors, Exceptions & Debugging with pdb? (one sentence, no jargon)
- When would you use it? (one concrete example)
- When would you NOT use it? (one anti-pattern)
What comes next
- Next session (S012): Modules, Packages, Virtualenvs, pip & uv
- Previous (S010): Inheritance, Composition & Polymorphism
- Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: (a) intuition · (b) visual walkthrough · (c) hands-on · (d) production reality · (e) quiz + exercise. Duration: 60 minutes.
More from M01 · Python Foundations
all modules →