Your pipeline processed 40,000 rows overnight — and died at row 40,001 on one malformed date, taking six hours of work with it. One bad record shouldn't kill a batch, and one missing file shouldn't crash an app. Exceptions are Python's failure-signaling system; try/except is how you decide which failures are survivable.
What you'll learn
What
An exception is an object raised when an operation can't proceed — int('abc') raises ValueError, {}['k'] raises KeyError. Unhandled, it travels up the call stack, printing a traceback and killing the program. try/except intercepts named exception types and runs recovery code instead.
Why
Real data WILL be malformed, files WILL be missing, and APIs WILL time out. The difference between a script and production software is what happens next. Data engineers live this daily: skip-and-count bad rows, retry flaky calls, fail loudly on genuine corruption — all built on try/except used with discipline.
Where it's used
Row-level parsing in ETL, file and network operations, user input validation, and every library call whose documentation says 'Raises: ...'.
Where this runs in production
Ingestion jobs wrap per-record parsing in try/except: bad records route to a dead-letter store with counts reported, and 10 million good rows survive 200 bad ones.
The Stripe SDK raises CardError, RateLimitError, APIConnectionError — distinct types so integrations can retry timeouts but never retry declined cards. Exception TYPE is the routing signal.
Flight software separates expected anomalies (handled, logged) from invariant violations (fail fast, alert humans) — the catch-narrowly discipline at its highest stakes.
def parse_age(text):
return int(text)
def build_profile(row):
return {'age': parse_age(row['age'])}
# build_profile({'age': 'abc'}) would print:
# Traceback (most recent call last):
# File "app.py", line 7, in <module> <- outermost call
# File "app.py", line 5, in build_profile
# File "app.py", line 2, in parse_age <- where it happened
# ValueError: invalid literal for int() with base 10: 'abc'Read tracebacks BOTTOM-UP: the last line names the type and message (the what), the line above it shows where it was raised (the where), and the frames above trace the call path (the how-we-got-here). Bottom line first, then climb only as needed — the single highest-value debugging habit.
An exception is a fire alarm pulled on floor 3 (the raising line). It doesn't stay there: the alarm travels UP floor by floor (the call stack), and on each floor someone either handles it (an except clause: 'small bin fire, extinguished, carry on') or lets it pass upward. If it reaches the roof unhandled, the whole building evacuates — program terminated, traceback printed. try/except is a floor saying 'alarms of THIS type, I know how to deal with.' Crucially, a floor that handles a kitchen-smoke alarm shouldn't also silence a gas-leak alarm — that's catching too broadly.
try: guards a block. If no exception occurs, except blocks are skipped entirely. If one occurs, execution STOPS at the failing line and jumps to the first except whose type MATCHES (isinstance — parent types catch child exceptions, so `except Exception` catches nearly everything). `except ValueError as e:` binds the exception object — always log or use e; recovery without recording is how data silently vanishes. Unmatched exceptions keep propagating as if the try weren't there.
The golden rule: name the specific exceptions you expect and have a plan for — except ValueError for parse failures, except FileNotFoundError for optional configs. A bare `except:` or blanket `except Exception:` also swallows typos (NameError), wrong types (TypeError), and Ctrl+C — converting loud bugs into silent wrong answers, the worst failure mode in data work. If you can't do something useful about it, don't catch it: an honest crash with a traceback beats a quiet corruption every time.
raw = ['21', '35', 'unknown', '42', '']
ages = []
skipped = 0
for value in raw:
try:
ages.append(int(value))
except ValueError:
skipped += 1
print(f'{len(ages)} parsed, {skipped} skipped')
print(ages)THE data-work pattern: try the risky operation per item, catch the SPECIFIC failure, count what you skip, report both numbers. One bad value no longer kills the batch — and the report keeps the data loss visible. (You've written the if-guard version of this; try/except handles failures you can't cheaply pre-check.)
Python culture favors EAFP ('easier to ask forgiveness than permission'): try the operation, handle the exception — versus LBYL ('look before you leap'): pre-check with if. EAFP wins when the check duplicates the operation (parsing: the only way to know if int() works is to try) or when state can change between check and use (files, network). LBYL wins for cheap, honest checks you already know: `if key in d`, `if items:`. Never stack both — checking AND catching the same failure is noise. And exceptions are for the EXCEPTIONAL: using try/except as routine control flow where an if is natural obscures intent.
int('abc') fails three calls deep. Click each stage to follow the alarm.
Survive a parse failure with a default.
The happy path returns from inside try; the failure path returns the default. Narrow catch: only ValueError — a TypeError (passing a list) would still crash loudly, as it should: that's a caller bug, not dirty data.
42 0 -1
Your task
Parse a messy gradebook: each record is (name, raw_score). Convert scores with float(); on ValueError append the name to a failures list. Print each successful student as '<name>: <score>', then the class average to 1 decimal, then 'failed to parse: <names>'.
ada: 95.0 mia: 88.5 raj: 73.0 average: 85.5 failed to parse: ['kai', 'zoe']
Write your solution in the editor on the right, then hit Run.
What happens to an exception no try/except ever matches?
Which line of a traceback do you read FIRST?
try: total += float(row['amt']) — which failures does `except ValueError:` handle?
What's the core lesson?
Write safe_divide(a, b) returning a/b, but None on ZeroDivisionError. Print safe_divide(10, 4), safe_divide(5, 0), and safe_divide(9, 3). Expected: 2.5 None 3.0
When does a `finally` block run?
What's the right guidance?
Walk me through what happens from the moment an exception is raised to the moment it's handled (or isn't).
At the raising line, normal execution stops immediately — no return value, no further statements in that block. Python creates the exception object and begins UNWINDING: the current function aborts, and control moves to its caller, checking each enclosing try for an except clause whose type matches (matching is isinstance-based, so parent classes catch subclass instances — `except Exception` catches nearly everything, which is exactly why it's dangerous). The first match wins: its handler runs, optionally binding the object with `as e`, and the program continues after the try statement — frames below have already unwound; frames above never know. Any `finally` blocks encountered during unwinding run on the way through, guaranteeing cleanup. If NO frame matches all the way out of main, the interpreter prints the traceback — every frame traversed, newest at the bottom — and exits nonzero. That bottom-up traceback order is why you read the last line first: it names the type, message, and raising location.
Why is `except Exception: pass` considered one of the worst patterns in data engineering specifically?
Because it converts the failure mode from loud-and-cheap to silent-and-compounding. In data systems, a crash is actually a GOOD outcome: it's immediate, it names the line, it stops wrong data at the source, and it pages someone. Blanket-catch-and-continue produces the opposite: the job reports success while producing empty or corrupt output, downstream tables and models consume it, and by discovery time you're rebuilding weeks of derived data and re-earning stakeholder trust. The pattern also flattens the crucial distinction between EXPECTED failures (dirty rows, missing optional files — handle, count, route to dead-letter) and BUGS (KeyError from a renamed column, TypeError from a bad refactor — must crash so they get fixed); catching Exception treats a schema change exactly like a malformed row. The discipline: catch the narrowest type you have an actual recovery plan for, keep failure counts visible in run summaries, and let everything else propagate. If the honest answer to 'what would I do with this exception?' is 'nothing useful' — don't catch it.
Explain EAFP vs LBYL and when you'd choose each in Python.
LBYL — look before you leap — pre-checks conditions with if: `if key in d: use(d[key])`. EAFP — easier to ask forgiveness than permission — attempts the operation and handles the exception: `try: use(d[key]) except KeyError:`. Python idiom leans EAFP for two solid reasons. First, some checks essentially duplicate the operation: the only reliable way to know whether int(s) will succeed is to run it — a validation regex would re-implement the parser, worse. Second, check-then-use has a race window when state can change between the two steps: os.path.exists() then open() can still raise FileNotFoundError if the file vanished in between, so the open() needs the try anyway and the pre-check adds nothing. LBYL stays right when the check is cheap, honest, and readable — `if items:` before indexing, `if x is not None`, dict `.get()` with a default (which is LBYL packaged as a method). Two anti-patterns to name: doing BOTH (redundant), and using exceptions as routine control flow where a plain if expresses intent better — exceptions are for the exceptional path, not the expected branch.
Common Mistakes to Avoid
1) Bare `except:` or blanket `except Exception:` — swallows bugs, Ctrl+C, everything; name your types. 2) Catch-and-pass with no logging or counting — data loss with no witness. 3) Giant try blocks — wrap the ONE risky line, so you know what you're guarding. 4) Catching exceptions you have no recovery plan for — an honest crash beats quiet corruption. 5) Whole-batch try around a loop — one bad row kills everything; put the try INSIDE the loop. 6) Pre-checking AND catching the same failure — pick EAFP or LBYL, not both. 7) Recovery code that can itself raise (using row['id'] in a KeyError handler) — handlers must be safe.
Ask the AI Tutor
Try these prompts in the AI Tutor panel: • 'Show me five tracebacks and quiz me on reading each bottom-up.' • 'Which exception type does each of these eight operations raise?' • 'Critique my try/except: is my catch too broad?' • 'Drill me on EAFP vs LBYL with real scenarios.' • 'Interview mode: ask me why except Exception: pass is dangerous and grade my answer.'
Glossary
Exception — an object raised when an operation can't proceed. Raise — signal a failure (used by Python and your own code). Traceback — the printed record of an unhandled exception's path; read bottom-up. Call stack — the chain of active function calls an exception unwinds through. try/except — guard a block; route matching exception types to handlers. as e — bind the exception object in a handler. else — runs only if try raised nothing. finally — runs on every exit path; cleanup's home. Narrow catch — handling specific expected types only. Dead-letter — routing failed records aside with reasons for replay. EAFP/LBYL — try-and-handle vs check-first styles.
Recommended Resources
• Docs: the 'Errors and Exceptions' tutorial chapter — the full statement forms in one page. • Read: the Built-in Exceptions reference intro — skim the hierarchy so 'except OSError' vs 'except FileNotFoundError' makes sense (FileNotFoundError IS-An OSError — your inheritance lesson, live). • Practice: take any loop you've written this course and make it survive one deliberately corrupted input, with counts reported. • Next in DSM: catching is half the story — Raising Exceptions teaches the other half: raise, custom exception classes, and designing the error contracts your own APIs expose.
Recap
✓ Exceptions are objects that unwind the call stack until a matching except (or the program dies). ✓ Read tracebacks bottom-up: type and message first, raising line second, path third. ✓ Catch NARROW types you can actually handle; let bugs crash loudly. ✓ Skip-and-count per row: try inside the loop, specific except, both numbers reported. ✓ else = success-only code; finally = every-exit cleanup. ✓ EAFP when checking duplicates the op or races it; LBYL when the check is cheap and honest. Next up: Raising Exceptions. You've caught what Python throws — now learn to throw well yourself: raise with precise types and messages, custom exception classes, and re-raising with context.
Run your code to see the output here.