PYTHON / ERRORS AND EXCEPTIONS
Exception chaining and reading tracebacks
Read a Python traceback from the bottom up and use raise ... from to attach, inspect, or suppress the original cause of an error.
What you will learn
- Locate the failing frame by reading the last frame and final line of a traceback
- Attach a root cause explicitly with raise NewError(...) from original_error
- Tell __cause__, __context__, and __suppress_context__ apart on a caught exception
- Use from None only when the inner error genuinely adds nothing
Understanding Exception chaining and reading tracebacks
A traceback is printed oldest frame first, so the line at the top is where the call started and the line just above the error message is where it actually blew up. Read it bottom-up: exception type and message first, then the last frame to see the guilty line, then walk upward only as far as you need to learn how that code was reached. Frames belonging to libraries are usually noise; the highest frame that lives in your own files is normally the one you can fix.
When an exception is raised while another one is being handled, Python does not throw the first one away. It stores it on the new exception's __context__ attribute automatically, and the printed traceback separates the two blocks with 'During handling of the above exception, another exception occurred'. That wording is a hint about causality: it means the second error happened inside an except block, which is often accidental, such as a typo in the handler itself.
raise NewError(...) from original is the deliberate version of the same link. It sets __cause__ to the original exception, sets __suppress_context__ to True, and changes the printed separator to 'The above exception was the direct cause of the following exception'. This is how you convert a low-level failure into a domain-specific one without destroying the evidence, and raise ... from None is the opposite switch: it keeps the chain objects but tells the traceback printer to hide them.
config = {"port": "8080x"}
class ConfigError(Exception):
pass
def read_port(cfg):
try:
return int(cfg["port"])
except (KeyError, ValueError) as err:
raise ConfigError("port must be an integer") from err
try:
read_port(config)
except ConfigError as exc:
current = exc
while current is not None:
print(f"{type(current).__name__}: {current}")
if current.__cause__ is not None:
print(" -> explicitly chained (__cause__)")
current = current.__cause__
elif current.__context__ is not None and not current.__suppress_context__:
print(" -> implicitly chained (__context__)")
current = current.__context__
else:
current = NonePython stores the previous exception on every new one, and raise ... from controls whether that link is presented as a deliberate cause or an accident inside a handler.
Worked examples
Implicit context versus from None
Shows that the original KeyError is stored either way, and that from None only flips the flag that hides it when printing.
def lookup(d, key):
try:
return d[key]
except KeyError:
raise RuntimeError("lookup failed")
def lookup_quiet(d, key):
try:
return d[key]
except KeyError:
raise RuntimeError("lookup failed") from None
for fn in (lookup, lookup_quiet):
try:
fn({}, "host")
except RuntimeError as exc:
print(fn.__name__)
print(" cause:", repr(exc.__cause__))
print(" context:", repr(exc.__context__))
print(" suppress:", exc.__suppress_context__)Example explained
Line 1Neither function uses `from err`, so __cause__ stays None in both cases.
Line 2__context__ is filled in by the interpreter because the RuntimeError was raised inside an active except block.
Line 3from None does not clear __context__; it only sets __suppress_context__ to True so the printer skips it.
Line 4Debugging tools can still reach the KeyError through __context__ even in the suppressed case.
Extracting frames instead of guessing
Uses traceback.extract_tb to prove that the last frame in a traceback is the one that raised.
import traceback
def outer():
middle()
def middle():
inner()
def inner():
return 1 / 0
try:
outer()
except ZeroDivisionError as exc:
frames = traceback.extract_tb(exc.__traceback__)
print("frames, outermost first:")
for frame in frames:
print(" ", frame.name)
print("raising line:", frames[-1].line)
print("exception:", type(exc).__name__, "-", exc)Example explained
Line 1exc.__traceback__ is the same frame chain that Python would print, available as an object.
Line 2extract_tb returns frames in call order, so index 0 is the try block and index -1 is where the error occurred.
Line 3frames[-1].line is the stripped source text of the failing statement, here the division.
Line 4The printed order matches the traceback, which is why the useful line sits at the bottom.
Important notes
The from clause needs an exception instance or None; passing a non-exception raises TypeError at the raise site.
Traceback text is not a stable API: line numbers, caret markers, and grouping vary between Python versions, so parse __cause__ and __traceback__ rather than the printed string.
Common mistakes
Reading the traceback top-down and 'fixing' the outermost call, when the real defect is in the last frame several calls deeper.
Writing raise ConfigError(str(err)) to carry the message forward; the chain is preserved anyway, and copying only the text loses the original type and its traceback.
Reaching for from None to make output tidy, which hides the root cause from whoever debugs the failure later.
Try it yourself
Change, predict, then run
Write a function parse_age that converts a string with int() and raises a custom ValidationError from the original ValueError, then call it with "twelve" and print type(exc).__cause__ along with its message.
Open the Python workspaceCheck your understanding
A traceback contains two exception blocks separated by 'During handling of the above exception, another exception occurred'. What does that tell you?
- The second exception was raised inside an except block, and Python linked it implicitly through __context__
- Someone wrote raise ... from err, deliberately naming the first exception as the cause
- Two exceptions were raised at the same time and grouped together
- The first exception was suppressed with from None and is only shown for reference
Show answer
That wording comes from __context__, which Python sets automatically when a new exception is raised while another is being handled, and it often signals a bug in the handler itself. Explicit chaining with raise ... from prints 'The above exception was the direct cause of the following exception' instead, so option two describes a different message.