Always Chain Exceptions With 'from e'
Two words in Python, from e, save your 2am debugging session. Catch an error, wrap it in a nicer one, re-raise it — and if you skip the from clause, the traceback your on-call self reads at 2am points at the wrapper, not the line that actually failed. Exception chaining has been in the language since Python 3.0 (PEP 3134), and it costs you exactly two words.
The failure mode
Here's the pattern that loses information:
try:
config = json.loads(raw)
except json.JSONDecodeError:
raise ConfigError("invalid config file") # original details buried
Python does keep the original exception here — as implicit context, printed under "During handling of the above exception, another exception occurred". But that message means "something else blew up while handling an error", which is ambiguous: was the second exception intentional, or a bug in your except block? Worse, tooling that only reports the final exception (many log aggregators, error trackers with default settings) shows you ConfigError: invalid config file and nothing else.
The two-word fix
try:
config = json.loads(raw)
except json.JSONDecodeError as e:
raise ConfigError("invalid config file") from e
Now Python attaches the original exception as __cause__ on the new one, and the traceback prints both, joined by an unambiguous line:
json.decoder.JSONDecodeError: Expecting ',' delimiter: line 3 column 9
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
...
ConfigError: invalid config file
The wrapper gives readers context ("config loading failed"); the chained cause gives them the root cause, file, and line. No guessing, no re-running with print statements.
The three chaining modes
| You write | Python sets | Traceback says |
|---|---|---|
raise NewError(...) inside except | __context__ (implicit) | "During handling of the above exception, another exception occurred" |
raise NewError(...) from e | __cause__ (explicit) | "The above exception was the direct cause of the following exception" |
raise NewError(...) from None | suppresses context | Only the new exception — the original is hidden |
Rules of thumb: use from e whenever the new exception is a deliberate translation of the old one (the overwhelmingly common case). Use from None only when the original is genuinely noise — for example, a KeyError inside a lookup helper where "not found" is the whole story. Bare raise (no argument) re-raises the current exception untouched and needs no chaining at all.
Power-user notes
- Lint for it. Ruff and flake8-bugbear flag a
raisewithoutfrominside anexceptblock as B904 (raise-without-from-inside-except); Pylint has the equivalentraise-missing-from. Turn one of them on and this tip enforces itself — including on AI-generated code, which frequently emits the unchained pattern. - Inspect the chain programmatically.
err.__cause__(explicit) anderr.__context__(implicit) are regular attributes — error-reporting middleware can walk the chain and log every link, and__cause__is writable if you need to attach a cause after the fact. fromaccepts a class too.raise TimeoutError("slow upstream") from ConnectionErrorinstantiates the class and attaches the instance as__cause__— handy in tests and small shims.__suppress_context__is whatfrom Noneactually sets. Since it's just an attribute, you can toggle it on an exception you've already caught before re-raising.- AI-agent angle: a coding agent debugging your codebase reads the same traceback you do. A chained traceback hands it the root cause in one shot; an unchained wrapper sends it (and its token budget) hunting.
Resources
- Python tutorial — Exception Chaining
- The
raisestatement —from,__cause__,__context__ - PEP 3134 — Exception Chaining and Embedded Tracebacks
- Ruff rule B904 — raise-without-from-inside-except
- Built-in exceptions —
BaseException.__cause__and friends
Watch the 60-second version — reel #126 in the Yeda AI Tips series — and follow for one concrete AI-coding tip per reel.