How do you log a caught Python exception with its full traceback rather than just its message?
answer
- The stack rides on the exception object
- Never log only the message
- logging takes a flag for this
- The traceback module has an _exc family
- logging.exception equals error plus exc_info
basics
~20 sInside the except block call logging.exception(msg), or pass exc_info=True to any logging call, so the record carries the active exception and its traceback. traceback.format_exc() returns that same text as a string; logging str(exc) throws the stack away.
solid answer
~40 sEvery exception instance carries its own traceback on `__traceback__`, so the stack is never lost — only discarded by how you log. Inside a handler, `logging.exception("msg")` is `logging.error("msg", exc_info=True)`: the logging machinery picks up the currently handled exception via `sys.exc_info()` and the formatter appends the rendered traceback to the record. If you need the text yourself, `traceback.format_exc()` returns it as a string and `traceback.print_exc()` writes it to stderr. For an exception you are holding rather than handling, `traceback.format_exception(exc)` renders that specific object. Logging `str(exc)` or an f-string of it gives you the message and not even the exception's type, which is why production logs so often read `KeyError` with no location — or nothing at all.
code
python · 11 linesimport logging
import traceback
logging.basicConfig(level=logging.ERROR, format="%(levelname)s %(message)s")
try:
{"legs": 3}["cost"]
except KeyError:
logging.exception("lookup failed") # record carries the traceback
text = traceback.format_exc() # same rendering, as a string
print(text.splitlines()[-1])go deeper
Recall two spellings and when each applies: logging.exception(msg) inside a handler, and traceback.format_exc() when you need the stack as a string. Be ready to say why logging just the message is not enough.
Explain the mechanics: exc_info=True makes the record capture the currently handled exception via sys.exc_info(), and the formatter renders it. Know that logging.exception is error plus that flag, and that it must run inside a handler.
Show judgement about where logging happens — once, at the boundary that decides the outcome — and how the stack reaches your log pipeline: formatException, structured fields, and rendering an exception you hold rather than one you are handling.
Own the house rule. Decide whether stacks are text blocks or structured fields, what the sampling and retention policy is for noisy handlers, and how error reporting stays consistent so one incident does not appear as five different failures.
### The stack is on the exception, not in the message When an exception propagates, CPython attaches a chain of traceback objects to the exception instance under the `__traceback__` attribute. That means the stack travels *with* the exception: by the time your `except` block runs, everything a reader needs is already sitting on the object you caught. Nothing about `except Exception as exc:` destroys the stack. Losing it is entirely a property of what you do next. `str(exc)` returns only the exception's message — usually `args[0]`. It does not include the exception's class name, the file, the line, or the calling frames. `logger.error(f"failed: {exc}")` therefore produces a log line that cannot be debugged: for a `KeyError` the message is just the missing key, and for many exceptions raised with no arguments it is the empty string. ### The logging route The `logging` module has first-class support for this. Every logging call accepts `exc_info`; when it is true, the `LogRecord` captures the exception triple and the formatter renders the traceback beneath the message. Two spellings matter: - `logging.exception("route rejected")` — shorthand for `logging.error("route rejected", exc_info=True)`. It is only meaningful **inside an exception handler**, because it reports the *currently handled* exception, the one `sys.exc_info()` returns. - `logger.warning("retrying", exc_info=True)` — the same capture at a level of your choosing. You may also pass the exception object itself, `exc_info=exc`, to log an exception you are holding rather than handling. Calling `logging.exception(...)` outside a handler is a classic mistake: `sys.exc_info()` returns `(None, None, None)` and the emitted record ends with the literal line `NoneType: None`. The rendering itself is done by `logging.Formatter.formatException`, which is simply a hook over the `traceback` module — override it if you want the stack as structured fields rather than a text block, which is what most JSON log formatters do. ### The traceback-module route When you want the text rather than a log record, the `traceback` module gives you it directly: - `traceback.format_exc()` returns the full rendered traceback of the currently handled exception as one string. - `traceback.print_exc()` writes the same thing to `sys.stderr`. - `traceback.format_exception(exc)` renders a specific exception object and returns a list of strings ending in newlines; `"".join(...)` gives the block. Since Python 3.10 this accepts the single exception argument shown here — older code passes the legacy three-argument `(type, value, tb)` form, which still works but is no longer the natural spelling. The `_exc` family reads the *current* exception, so call them inside the handler. Once the `except` block has exited, the handled exception is no longer current and `format_exc()` will report `NoneType: None` just like the logging equivalent. ### What the rendered block contains A rendered traceback lists one entry per frame, oldest (outermost) first, ending at the line that raised, followed by the exception's type and message. Since Python 3.11 (PEP 657) each entry can carry caret anchors — the `~~~^^^` markers under the exact sub-expression that failed — which is why a traceback from a modern interpreter can pinpoint *which* of several subscripts in one line blew up. Because those anchors are derived from the source line, they only appear when the source file is still readable at render time. ### Practical rules Log the exception once, at the layer that decides what to do about it, and let it propagate otherwise — an `except`/`log`/`re-raise` at every level multiplies the same stack across your logs. Never write `except Exception: pass`; if you genuinely want to ignore a failure, ignore a *specific* exception and say why in a comment. And prefer `logging.exception` over hand-assembling a stack from frame attributes: the module already handles recursion limits, long chains and the caret anchors correctly.
- What appears in the log if you call logging.exception outside any except block?The message is emitted normally, but because `sys.exc_info()` returns `(None, None, None)` the formatter appends the literal line `NoneType: None` instead of a stack. It is a common symptom of logging from a helper called after the handler has already exited — pass the exception explicitly with `exc_info=exc` in that case.
- How would you get the traceback into a JSON log line rather than a multi-line text block?Subclass `logging.Formatter` and override `formatException` (or build the record dict yourself) so the rendered frames go into a field instead of being appended to the message. `traceback.TracebackException.from_exception(exc)` is the convenient source: it exposes the type, message and frame list so you can emit them as structured data.
- Why is catching, logging and re-raising at every layer discouraged?The same failure is then rendered once per layer, so one incident produces several near-identical stacks and readers cannot tell whether they are looking at one fault or many. Log where you decide what to do about the failure — usually a request or job boundary — and let the exception propagate untouched elsewhere.
The exception is a parcel with the delivery route printed on the label; printing only str(exc) is copying the recipient's name onto a fresh envelope and throwing the label away.
saying these in an interview costs you the question
- Logs str(exc) and calls that error handling
- Thinks logging.exception works anywhere, not only inside a handler
- Believes the exception message contains the stack
- Calls traceback.format_exc() after the except block has exited
- Assembles the stack by hand from frame attributes
- Catches, logs and re-raises at every single layer