skip to content

What does an exception's __traceback__ attribute hold, and how do you walk it?

level: middleimportance: should knowfreq 44%

answer

  1. It is an object, not a string
  2. A linked list, one node per frame
  3. One attribute points further in
  4. Outermost frame is the head
  5. extract_tb gives inert summaries

basics

~20 s

It holds a traceback object, not text: a singly linked list of frames. Each node exposes tb_frame, tb_lineno and tb_lasti, and tb_next points one step deeper toward the raise. Walk it by following tb_next until it is None.

solid answer

~40 s

`BaseException.__traceback__` is a `types.TracebackType` object — a linked list whose head is the **outermost** frame the exception passed through and whose `tb_next` chain runs inward to the frame that raised. Each node carries `tb_frame` (a live frame object, with its code object and `f_locals`), `tb_lineno` (the line executing in that frame) and `tb_lasti` (the bytecode offset, which is what the caret anchors are computed from). Inside a handler, `sys.exc_info()` returns `(type(exc), exc, exc.__traceback__)` and `sys.exception()` — added in 3.12 — returns just the instance. `BaseException.with_traceback(tb)` sets the attribute and returns the exception, so it composes into a `raise` statement. Rather than walking by hand, `traceback.extract_tb(tb)` gives a `StackSummary` of `FrameSummary` objects with filename, line number, function name and source line already resolved.

code

python · 16 lines
python
import sys

def leg_cost(n):
    return 100 / n

def plan(legs):
    return [leg_cost(n) for n in legs]

try:
    plan([4, 0])
except ZeroDivisionError as exc:
    print(sys.exc_info()[2] is exc.__traceback__)
    tb = exc.__traceback__
    while tb is not None:
        print(tb.tb_frame.f_code.co_name, tb.tb_lineno)
        tb = tb.tb_next

go deeper

for a junior

It is enough to know that the exception you catch carries a traceback object under __traceback__ and that the traceback module turns it into readable text. You will not be asked to walk it by hand.

for a middle

Be ready to describe the node: tb_frame, tb_lineno, tb_next, and the direction the chain runs. Explain that nodes are added during propagation, and reach the same data from a handler through sys.exc_info() or sys.exception().

for a senior

Show you know the operational consequences: a raw tb_frame pins that frame's locals, so prefer extract_tb or TracebackException when anything is stored, forwarded across a boundary or shipped to an error reporter.

for a principal

Decide what your platform records about a failure and what it must not: frame locals can carry credentials and personal data, so the difference between shipping rendered frames and shipping live frames with locals is a policy call, not a coding preference.

### A traceback is a data structure, not a string The text you see on a crash is a *rendering*. The thing attached to the exception is an object of type `types.TracebackType`, and it is a singly linked list. Every node has four public attributes: - `tb_frame` — the frame object for that level of the call stack, from which you can reach the code object (name, filename) and the frame's local variables. - `tb_lineno` — the line that was executing in that frame when the exception passed through it. - `tb_lasti` — the index of the bytecode instruction being executed, which is how the interpreter recovers the exact column range for the caret anchors introduced in 3.11. - `tb_next` — the next node, one step **closer to the raise**, or `None` at the end. So the head of the list is the outermost frame and the tail is where the exception was raised. The rendered text is printed in the same order, which is why the culprit is at the bottom of a Python traceback rather than the top. ### Where the nodes come from The list is not built at construction time. It is built during propagation: as the exception leaves each frame, the interpreter creates a new traceback node for that frame and links it in front of the existing chain. Consequently the traceback only contains frames the exception actually travelled through — it stops at the frame that catches it. Frames *above* the `try` are on the call stack but never appear on the exception, which surprises people who expect a full stack dump. If you want the callers as well, that is `traceback.print_stack()` or logging's `stack_info=True`, not the exception's traceback. ### Reaching it from a handler Three spellings coexist: - `exc.__traceback__` — direct attribute access on the exception you caught; the modern default. - `sys.exc_info()` — returns the legacy triple `(type, value, traceback)` for the exception currently being handled, and returns `(None, None, None)` outside a handler. The third element is the very same object as `exc.__traceback__`. - `sys.exception()` — added in Python 3.12, returns just the exception instance being handled, or `None`. It is the tidy replacement for `sys.exc_info()[1]`. Going the other way, `BaseException.with_traceback(tb)` assigns the attribute and **returns the exception itself**, which is what makes `raise NewError(...).with_traceback(old_tb)` a single expression. It does not raise anything on its own. ### Reading it without hand-walking Walking `tb_next` yourself is instructive but rarely what you want in production code, because a raw frame lets you touch live locals and keeps memory alive. The `traceback` module gives you resolved, inert views: - `traceback.extract_tb(tb)` returns a `StackSummary` — a list of `FrameSummary` objects with `filename`, `lineno`, `name` and the source `line` already read from disk. - `traceback.format_tb(tb)` returns those entries already rendered as strings. - `traceback.TracebackException.from_exception(exc)` captures the whole thing, including the type and message, in a form that no longer references frames. A `FrameSummary` is safe to keep; a `tb_frame` is not, because it pins that frame's locals in memory for as long as you hold it. ### Why the anatomy matters in interviews Knowing the shape explains several behaviours at once: why the culprit is printed last, why the traceback of a caught exception starts at the `try` rather than at `main`, why re-raising makes the list grow, why you can transplant a traceback onto a different exception, and why holding an exception object holds much more memory than its message. It also tells you where the limits of the object are: the traceback records *where*, never *what the values were* at each step — for that you need the frame's `f_locals`, which is precisely the thing that makes retaining tracebacks expensive.

  • Why does a caught exception's traceback not show the frames above the try block?
    Nodes are appended only as the exception propagates out of a frame, so the chain covers exactly the frames between the `try` and the `raise`. Callers further up never saw the exception, so they never contributed a node. To record where you were called from, use `stack_info=True` on a logging call or `traceback.print_stack()`, which walk the live call stack instead.
  • What is the difference between sys.exc_info() and sys.exception()?
    `sys.exc_info()` returns the legacy triple `(type, value, traceback)` and gives `(None, None, None)` outside a handler. `sys.exception()`, added in 3.12, returns only the exception instance, or `None`. Since the type is `type(exc)` and the traceback is `exc.__traceback__`, the triple carries nothing extra — prefer `sys.exception()` in new code.
  • Does with_traceback raise the exception it is called on?
    No. `BaseException.with_traceback(tb)` sets `__traceback__` and returns the same exception object, so it is an expression you can hand to a `raise` statement. On its own it is a mutation and nothing else — code that calls it and discards the result has silently changed the exception's traceback.

Think of it as a chain of luggage tags added at each airport the bag passed through: the first tag is where the journey started, and following the chain to the end tells you exactly where it went missing.

saying these in an interview costs you the question

  • Says __traceback__ holds a formatted string
  • Thinks the traceback lists the whole call stack, callers included
  • Reads tb_next as pointing toward the caller
  • Believes with_traceback raises rather than returns
  • Thinks sys.exception() returns the same triple as sys.exc_info()
  • Assumes the traceback records variable values at each frame

context