Search Tech Journey

Find topics, journeys and posts

6-month learning plan11 / 130
back to blog
pythonbeginner 15m read

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)


(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:

ClauseRuns whenTypical use
tryalways — the guarded blockthe risky call
excepta matching exception was raisedrecovery / logging
elsetry finished with no exceptioncode that depends on try success
finallyalways, even if exception uncaught / returncleanup (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()           # always

pdb cheat-sheet (drop import pdb; pdb.set_trace() or breakpoint() anywhere):

CommandMeaningCommandMeaning
nnext linep xprint value of x
sstep into callllist source around here
ccontinue to next bpwwhere — show stack
qquitpp xpretty-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:

  1. good order prints order A1 total=25.00.
  2. bad order logs an ERROR bad order B2 with a full traceback including the raise ... from e chain (see The above exception was the direct cause…).
  3. finally runs for both orders — you'll see done with order twice.
  4. The process exits code 0 — we caught OrderError, we didn't crash.
  5. Uncomment breakpoint() and rerun: at the prompt type p 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 (logging with JSON formatter, or structlog) so tracebacks are queryable in Kusto / Splunk / Elasticsearch.
  • faulthandler for segfault-style crashes in native extensions.
  • py-spy / pyspy dump --pid N to 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

  1. In a traceback, which line is the actual error — the top or the bottom?
  2. What is the difference between except Exception and except BaseException, and why does it matter for Ctrl-C?
  3. When would you use else in a try/except/else/finally block instead of putting the code inside try?
  4. What does raise NewError("msg") from e do that raise NewError("msg") does not?
  5. 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:

  1. What is Errors, Exceptions & Debugging with pdb? (one sentence, no jargon)
  2. When would you use it? (one concrete example)
  3. When would you NOT use it? (one anti-pattern)

What comes next


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 →
  1. 005Python Variables & Types — Mental Model of Memory
  2. 006Control Flow — if/else, loops, comprehensions
  3. 007Functions — arguments, scope, closures
  4. 008Data Structures — list, tuple, dict, set (when to use what)
  5. 009Classes & Objects — the OOP Mental Model
  6. 010Inheritance, Composition & Polymorphism