Python Error Handling: try / except / else / finally in 2026
Error handling separates "code that works on your machine" from "code that survives production". Python's try / except looks simple, but the four-keyword full form — try / except / else / finally — encodes a surprising amount of structure most tutorials skip.
This guide covers: what each keyword actually means, the six patterns you'll see in every backend codebase, when to define your own exceptions, and the three anti-patterns that let bugs slip past your tests.
The four keywords, precisely
The else clause is under-used. It says "this next bit depends on the try succeeding, but I don't want it protected by the same except". It's the right place for the happy path continuation — putting it inside try accidentally catches unrelated exceptions from ship(result).
The six patterns
1. Retry on transient failure
Rule: retry ONLY on transient errors (network, timeout, 429, 503). Never retry on ValueError, KeyError, TypeError — those are code bugs, and retrying just runs the bug three times.
2. Guard with else
The else guarantees file was opened successfully AND scopes cleanup with with. Notice: process() might raise ValueError — that will propagate up, NOT be caught by except FileNotFoundError.
3. Cleanup with finally (before with existed)
In modern code you'd use with db.connect() as conn: — with is the sugar over try / finally for the common cleanup case. Reach for explicit finally only when the resource doesn't support the context-manager protocol.
4. Re-raise with added context
raise NewException(...) from e chains the original exception onto the new one so the traceback shows BOTH. Never write raise NewException(str(e)) — that discards the original traceback.
5. Catch the narrowest exception you can
Rule: the narrower the except, the better. If you don't know what process can raise, run it and find out. except Exception: is a code smell 90% of the time.
6. raise inside except — re-raise cleanly
Bare raise keeps the original traceback intact. Never raise e (Python 3 is smart about it, but bare raise is the idiomatic form).
Custom exceptions — when it's worth it
Define your own exception class when:
1. Callers need to except MyError: specifically (i.e., you have a callable API and users need to branch on failure type).
2. Your library / service raises multiple related errors that share context.
Now callers can catch broadly (except PaymentError) or narrowly (except CardDeclinedError). Structured attributes on the exception (e.card_last4) let error handlers build useful UX without parsing exception messages.
Anti-pattern: defining a custom exception that only exists to be raised once and never caught. That's just raise ValueError("..."). Custom exceptions earn their weight from being caught.
Three anti-patterns that hide bugs
Anti-pattern 1: Bare except
Bare except catches signals that should exit the program (Ctrl-C, sys.exit, MemoryError). Never write this. If you truly want to catch every non-fatal exception, use except Exception: — which STILL catches too much, but at least respects Ctrl-C.
Anti-pattern 2: Silencing without logging
Every caught exception should either be handled (with a specific fallback) or logged. pass in an except is a bug in slow motion — the code appears to work while silently discarding data.
Anti-pattern 3: except Exception: around everything
Problem: if stripe.charge succeeded but send_receipt failed, the customer got charged AND you swallowed the error. The except covers four different failure modes, and you can't tell them apart.
Fix: narrow the try scope + branch the except by module boundary.
Each try covers ONE operation. Each except knows what to do about THIS specific failure. That's production-grade error handling.
Good error handling is more design than syntax. try / except / else / finally gives you the tools; the discipline is picking the right except type, the narrowest possible try scope, and never silencing errors you don't understand.
Next step: the Foundations track walks through exception handling, custom exceptions, and the with statement — with real Python running in your browser.