DSM
60 XP
30 minsIntermediate60 XP

Exceptions & try/except

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

  • Read a traceback bottom-up and name the exception type
  • Explain how exceptions propagate up the call stack
  • Catch NARROW exception types with try/except
  • Use else and finally for the full statement shape
  • Apply the skip-and-count pattern to dirty data

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

AirbnbRow-level resilience in ETL

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.

StripeTyped API errors

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.

NASA JPLFail loudly on the impossible

Flight software separates expected anomalies (handled, logged) from invariant violations (fail fast, alert humans) — the catch-narrowly discipline at its highest stakes.

Theory

The core ideas, in plain language.

You've met exceptions all course: ValueError from int('abc'), KeyError from a missing dict key, TypeError from 'a' + 1, IndexError, ZeroDivisionError, AttributeError, FileNotFoundError. Each is an OBJECT (an instance of an exception class — the inheritance module in action) carrying a message and a traceback. Raising is how Python — and your own setters — say 'I cannot do this'.
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.

Analogy: The fire alarm in an office tower

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.

Key Concept
The try/except mechanics

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.

Key Concept
Catch narrowly, catch what you can HANDLE

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.)

Watch out
EAFP vs LBYL — and when each wins

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.

Visual Learning

See the concept, then explore it.

An exception's journey up the stack

int('abc') fails three calls deep. Click each stage to follow the alarm.

Worked Examples

Watch it built up, one line at a time.

Very EasyFirst catch

Survive a parse failure with a default.

Step 1 of 1

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.

Code
01def safe_int(text, default=0):
02 try:
03 return int(text)
04 except ValueError:
05 return default
06 
07print(safe_int('42'))
08print(safe_int('n/a'))
09print(safe_int('n/a', default=-1))
Output
42
0
-1
Practice Coding

Your turn — write the code.

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>'.

Expected output
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.

Exercises

Prove it. Reach 80% to complete the lesson.

Mastery Gate0% / 80% required
Easy0/2 solved

What happens to an exception no try/except ever matches?

Which line of a traceback do you read FIRST?

Medium0/3 solved

try: total += float(row['amt']) — which failures does `except ValueError:` handle?

ScenarioA teammate's nightly job wraps its entire 300-line main() in try: ... except Exception: pass — 'so it never crashes'. It has reported success for three weeks. Today you discover it stopped writing output 19 days ago after a renamed column caused a KeyError on line 12.

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

Narrow catch:Only ZeroDivisionError is handled; TypeError would still propagate
Value-or-None contract:The failure path returns None explicitly
Hard0/2 solved

When does a `finally` block run?

ScenarioCode review: for f in files: try: data = load(f); results.append(transform(data)); except FileNotFoundError: continue — and the author asks whether to ALSO wrap transform's occasional TypeError 'to be safe'.

What's the right guidance?

Complete 80% more exercises to unlock.
Interview Prep

How this shows up in real interviews.

Walk me through what happens from the moment an exception is raised to the moment it's handled (or isn't).

Show model answer

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?

Show model answer

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.

Show model answer

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.

scratchpad — preview this lesson's challenge anytime
grade_parser.pyPython
Ready
Output

Run your code to see the output here.