skip to content

Formatting Tracebacks

Reading a traceback from the bottom up and producing one yourself: formatting the current exception as text, capturing a failure as an object you can send elsewhere, and printing a chain.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

What does traceback.format_exc() return, and how does traceback.print_exc() differ?

level: juniorimportance: must knowfreq 68%

answer

  1. One returns text, one writes it
  2. Neither takes an exception argument
  3. Both read the currently handled exception
  4. String versus a write to sys.stderr
  5. logging.exception does the same rendering

basics

~20 s

traceback.format_exc() returns the full report for the exception currently being handled as one string. traceback.print_exc() renders the same text but writes it to sys.stderr and returns None. Both read the current exception, so call them inside an except block.

solid answer

~40 s

Both read the *currently handled* exception — what `sys.exception()` returns, or the triple from `sys.exc_info()` — so they belong inside an `except` block. `traceback.format_exc()` hands you the whole report as a single newline-terminated `str`, byte-for-byte what the interpreter would have printed had the exception gone unhandled. `traceback.print_exc()` writes that same text to `sys.stderr` (or to the object passed as `file=`) and returns `None`. Both accept `limit=` to cut the number of stack frames and `chain=True` (the default) to include the earlier exceptions reachable through `__cause__` and `__context__`. In a service you rarely want either directly: inside the handler, `logging.exception("...")` produces the same rendered traceback and routes it through your handlers and formatters. Called with no exception being handled, `format_exc()` does not raise — it returns a placeholder string.

code

python · 13 lines
python
import traceback

def parse_offset(raw):
    return int(raw)

report = ""
try:
    parse_offset("+00:00")
except ValueError:
    report = traceback.format_exc()

print(report.splitlines()[-1])
print(len(report.splitlines()), "lines in the report")

go deeper

for a junior

Be ready to say which one returns a string and which one writes to a stream, and to show the call sitting inside an except block. Knowing that both act on the exception currently being handled is the whole point of the question.

for a middle

Explain where the input comes from — the currently handled exception via sys.exception or sys.exc_info — and what limit and chain do. Show that you would reach for logging.exception in application code instead.

for a senior

Demonstrate the operational judgement: raw writes to sys.stderr escape your logging configuration, and a report captured as text loses the structure a log pipeline wants. Explain how you route tracebacks in a real service.

for a principal

Own the convention: pick one traceback path for the whole codebase, decide whether reports are rendered text or structured fields, and make sure whatever you choose reaches the place engineers actually look during an incident.

### The "current exception" is the input Neither function takes an exception argument. Both look at the exception the interpreter is presently handling: since 3.11 you can read it directly with `sys.exception()`, and the older `sys.exc_info()` returns the `(type, value, traceback)` triple. That state is set while an `except` block — or a `finally` clause reached during handling — is executing, and it is per-thread. So the honest rule is: **call these two from inside a handler.** Call `traceback.format_exc()` when nothing is being handled and it will not raise; it returns the placeholder text for a null exception rather than blowing up in your logging path. That is convenient and also a trap, because a report that says nothing useful is easy to ship to production unnoticed. ### What each one gives back `traceback.format_exc()` returns **one `str`**. It contains the `Traceback (most recent call last):` banner, one `File "...", line N, in func` pair per frame with the source line beneath it, and finally the exception type and message. The string ends in a newline, so `print(traceback.format_exc())` produces a blank line at the end; `sys.stderr.write(traceback.format_exc())` does not. `traceback.print_exc()` renders the identical text and writes it, returning `None`. Its `file=` parameter takes any writable text stream, so `print_exc(file=buf)` with an `io.StringIO` is a long-winded way of writing `format_exc()`. The default destination is `sys.stderr`, which matters operationally: in a container that only ships stdout, a `print_exc()` report can vanish. ### The two shared parameters `limit` bounds how many stack frames are rendered. A positive `limit` keeps the frames closest to the top of the stack, a negative one keeps the innermost frames — usually the interesting end, since that is where the failure actually happened. `chain` defaults to `True`, which walks the exception's `__cause__` and `__context__` links and prints the earlier exceptions first, separated by the standard banner lines. Passing `chain=False` prints only the exception in hand and drops everything it was raised from. ### Reading the output The report is ordered outermost call first, innermost last, and the exception type and message sit on the final line. That is why the advice is to read a traceback **bottom-up**: the last line tells you what went wrong, the `File` line directly above it tells you where, and the frames above that tell you how you got there. In a chained report the *lowest* block is the exception that was actually raised out of the handler. ### Where these belong in real code In a script or a CLI, `print_exc()` in a top-level handler is perfectly reasonable. In a long-running service it is usually the wrong tool: the text goes to a stream your logging configuration does not own, unfiltered and unstructured. Inside the handler, `logging.exception("ingest failed")` — or `logger.error(msg, exc_info=True)` — attaches the same rendered traceback to a real log record with a level, a logger name and a timestamp, and lets your formatter decide the shape. Where you need the text as a value — to attach to an error payload, or to compare two failures — `format_exc()` is the right call. The answer an interviewer is really listening for is the negative one: **not** `print(exc)` and **not** `logging.error(str(exc))`. `str()` of an exception is the message alone — no type, no frames, no chain — and a log full of `invalid literal for int()` with no location is the most common self-inflicted diagnostic wound in Python services. ### One shape difference worth remembering `format_exc()` returns a single string; the sibling function `traceback.format_exception()` returns a **list of strings** that you join to get the same text. Different shapes for the same content trips people up when they hand the result straight to a formatter that expects one or the other. ### Rendering is not free Producing the text means reading the source file for every frame through the `linecache` module so each `File` line can carry its source underneath. On a cold cache that is real file I/O, performed on the failing path. For an exception that fires a handful of times a day this is irrelevant; for one that fires on every malformed record in a batch, formatting a full report per failure is measurable, and the usual answer is to log a short summary per occurrence and a full report only for the first of each kind. ### It is per-thread The currently handled exception is thread-local state. A helper that calls `format_exc()` will report *its own* thread's handler, and a callback dispatched onto another thread sees nothing. That is another reason the two functions are best called in the handler itself rather than deep inside a utility that may be invoked from anywhere.

  • What happens if you call traceback.format_exc() when no exception is being handled?
    It does not raise. With no current exception the triple behind it is all `None`, and you get back a short placeholder report instead of a real traceback. That is why a stray `format_exc()` outside a handler quietly produces useless log lines rather than an obvious failure.
  • Inside an except block, why prefer logging.exception over traceback.print_exc?
    `logging.exception` builds a real log record: it carries a level, a logger name, a timestamp and your formatter's structure, and it goes wherever your handlers point. `print_exc` writes raw text to `sys.stderr`, bypassing filters, levels and formatters, which in a containerised service often means the report lands somewhere nobody is collecting.
  • What does the limit parameter do on these two functions?
    `limit` bounds how many stack frames are rendered. A positive value keeps frames from the outermost end, a negative value keeps the innermost ones — usually what you want, since the failure is at the bottom. The exception type and message line is always printed regardless of `limit`.

saying these in an interview costs you the question

  • Claims format_exc takes the exception as an argument
  • Says print_exc returns the traceback string
  • Logs str(exc) and calls that a traceback
  • Thinks print_exc writes to stdout by default
  • Expects format_exc to raise outside a handler

context

open as a page

What does traceback.extract_tb() return, and when do you want it over rendered text?

level: middleimportance: should knowfreq 36%

basics

~20 s

traceback.extract_tb() turns a traceback object into a StackSummary: a list of FrameSummary items carrying filename, lineno, name and the source line. You want it when the failure must become structured fields rather than one blob of text.

open as a page

What does traceback.format_exception() return when passed an exception object?

level: middleimportance: should knowfreq 44%

basics

~20 s

It returns a list of strings that concatenate to exactly the report the interpreter would print. Since Python 3.10 you pass just the exception instance; by default it also renders the exceptions reachable through cause and context.

open as a page

Why capture a failure as a traceback.TracebackException instead of keeping the exception?

level: seniorimportance: should knowfreq 32%

basics

~10 s

traceback.TracebackException.from_exception() pre-renders everything a report needs — frames, source lines and the chained exceptions — and keeps no live frames. Holding the exception instead pins every frame's locals in memory until you release it.

open as a page