try/except isn't just "catch errors so the program doesn't crash" — where you catch, what you catch, and whether you re-raise are all real design decisions with different failure modes.
4 min read
def parse_age(raw):
return int(raw)
try:
age = parse_age("thirty")
except ValueError:
age = 0Raising an exception immediately stops normal execution and unwinds the call stack — frame by frame — looking for a try block whose except matches the exception's type, until either a match is found or the exception reaches the top of the program and crashes it. int("thirty") raises ValueError from inside parse_age; that function has no try, so the exception propagates up and out of it to the caller's except ValueError, skipping every remaining line in parse_age entirely — int(raw) never returns normally, execution just leaves that function.
except: is a real bug, not just bad styletry:
result = risky_operation()
except: # catches EVERYTHING, including KeyboardInterrupt and SystemExit
result = NoneA bare except: catches KeyboardInterrupt (Ctrl+C) and SystemExit, which are exceptions in Python's hierarchy specifically so cooperative code can choose to clean up on shutdown — but a blanket catch silently swallows the user's Ctrl+C or a deliberate sys.exit(), making the program impossible to interrupt normally. It also hides genuine bugs: a TypeError from a typo gets silently converted into result = None instead of surfacing as a crash you'd actually notice and fix. except Exception: is the usual "catch broadly but not that broadly" choice — it excludes KeyboardInterrupt and SystemExit, which both inherit from BaseException but not Exception.
try:
value = data[key]
except KeyError:
value = default
except (TypeError, AttributeError):
value = NonePython checks except clauses top to bottom and uses the first one whose type matches (via isinstance, so a subclass matches its parent's except too) — which is why a more specific exception type must be listed before a more general one that's also its parent class; an except Exception listed first would swallow every subclass beneath it, and any except clauses after it would be unreachable dead code.
raise vs. raise ... from: preserving the real causedef load_config(path):
try:
with open(path) as f:
return json.load(f)
except FileNotFoundError as e:
raise ConfigError(f"missing config: {path}") from eRe-raising a different, more meaningful exception is common — callers of load_config shouldn't need to know it's implemented with json.load under the hood. from e explicitly chains the original exception as the new one's __cause__, so the traceback shows both: "this ConfigError happened, caused by this underlying FileNotFoundError" — genuinely useful when debugging, versus a bare raise ConfigError(...) which discards the original traceback context entirely (Python still shows it as "during handling of the above exception," implicitly, via __context__, but from e makes the causal relationship explicit and intentional rather than incidental).
finally: the one block that always runsdef process(conn):
try:
return conn.execute(query)
finally:
conn.close()finally runs whether the try block succeeds, raises, or even hits a return inside it — this is exactly the guarantee a context manager's __exit__ relies on internally, and why finally is the right place for cleanup that absolutely must happen (closing a connection, releasing a lock) regardless of how the block exits. It runs before the return value is actually handed back to the caller, and before an unhandled exception continues propagating outward.
StopIteration (ending a for loop), KeyError/IndexError (distinguishing "missing" from a real value in patterns like dict.get), and even generator cleanup all use exceptions as the mechanism, not just for genuine failures — this is a deliberate language design choice ("easier to ask forgiveness than permission," EAFP), distinct from languages that treat exceptions as exclusively for exceptional, unrecoverable failures.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why is a bare `except:` considered a real bug, not just bad style?
2. In a try block with multiple except clauses, why must a more specific exception type be listed before its more general parent class?
3. What does `raise ConfigError(...) from e` do that a bare `raise ConfigError(...)` doesn't?
4. When does a `finally` block run relative to a `return` inside the `try`?
Python