skip to content

What does logging.exception() record that logging.error() alone does not?

level: juniorimportance: must knowfreq 58%

answer

  1. One of them attaches more than a message
  2. Think about what a level shortcut hides
  3. It only means something inside except
  4. exc_info=True, done for you, at ERROR
  5. stack_info answers a different question

basics

~10 s

logging.exception() logs at ERROR level and attaches the traceback of the exception currently being handled, because it passes exc_info=True for you. logging.error() logs only the message unless you pass exc_info=True yourself.

solid answer

~40 s

`logging.exception(msg)` is exactly `logging.error(msg, exc_info=True)`: same ERROR level, plus the type, value and traceback of the exception being handled right now. It is only meaningful **inside an `except` block** — called anywhere else it appends the line `NoneType: None`, because there is no active exception to describe. `exc_info` also accepts an exception instance or a `(type, value, traceback)` tuple, so you can attach a traceback to any level: `log.warning(msg, exc_info=exc)` or `log.critical(msg, exc_info=True)`. A separate keyword, `stack_info=True`, attaches the call stack that reached the logging call itself, with no exception involved — that answers "who called this?", not "what blew up?". Put the context the traceback lacks (which record, which batch, which URL) in the message; the traceback already carries the exception text.

code

python · 11 lines
python
import logging

logging.basicConfig(level=logging.INFO)
log = logging.getLogger("inventory.sync")

try:
    int("6,800")
except ValueError:
    log.error("row count unparsable")                   # message only
    log.exception("row count unparsable")               # message + traceback
    log.error("row count unparsable", exc_info=True)    # identical to the line above

go deeper

for a junior

Recall the identity: logging.exception() is logging.error() plus exc_info=True, and it belongs inside an except block. Be ready to say what extra text lands in the log because of it.

for a middle

Explain what exc_info resolves to, that it also accepts an exception instance or a triple, and that stack_info attaches the caller's stack instead of a traceback. Know why a %-style message beats an f-string.

for a senior

Show judgement about level: a swallowed, retriable failure is a WARNING with a traceback, not an ERROR. Talk about putting identifying context in the message and letting the traceback carry the rest.

for a principal

Own the log-shape contract: one exception should produce one structured record with the traceback as a field, not text glued into a message, so records can be grouped, redacted and routed consistently across services.

## The one-line identity `Logger.exception(msg, *args)` is a convenience wrapper. Its entire behaviour is: log at `ERROR` level with `exc_info=True`. Nothing else. Writing `log.exception("sync failed")` and `log.error("sync failed", exc_info=True)` produces byte-identical output. The method exists because logging a caught exception without its traceback is the single most common logging mistake, and a dedicated verb makes the right thing shorter than the wrong thing. ## What `exc_info` actually resolves to When `exc_info` is truthy, the logging machinery captures the *currently handled* exception — the same thing `sys.exc_info()` returns — and stores the `(type, value, traceback)` triple on the log record. A formatter later renders it as the familiar `Traceback (most recent call last): ...` block underneath the message. "Currently handled" is the crucial qualifier. Inside an `except` block there is one; outside, there is not. Call `log.exception("oops")` at module level and you get the message followed by the literal line `NoneType: None` — a puzzling artefact that always means *you logged a traceback where no exception was live*. The same applies after the `except` block has finished: Python clears the exception state on the way out, so a deferred `log.exception()` records nothing useful. `exc_info` is not limited to `True`. It also accepts: - an **exception instance** — `log.error("deferred", exc_info=exc)`, useful when you stashed the exception and log it later, or when you are reporting an exception you never caught in this frame (one pulled off a queue, or a `Future`'s stored exception); - an explicit **`(type, value, traceback)` tuple** — what a top-level hook such as `sys.excepthook` is handed. Because `exc_info` is an ordinary keyword on every level method, the traceback is not welded to ERROR. A retriable failure that you intend to swallow is honestly a `WARNING` *with* a traceback; a failure that is about to kill the process is `CRITICAL` with one. `logging.exception()` is only the ERROR-level shorthand, and reaching for it purely to get the traceback is how logs end up full of ERROR entries for things that were handled fine. ## `stack_info` is a different question `stack_info=True` is often confused with `exc_info` because both append a multi-line block. They answer different questions: - `exc_info` → *where did the exception come from?* It renders the traceback of a raised exception, unwinding from the `raise` site. - `stack_info` → *how did control reach this logging call?* It renders the current call stack, headed `Stack (most recent call last):`, whether or not an exception exists. `stack_info` earns its keep on the puzzling non-exceptional log line: a warning that fires from a helper called in a dozen places, a deprecation notice, a "cache miss" you cannot attribute. It costs a stack walk plus formatting per call, so it belongs on rare lines, not on a hot path. ## Why not just format the traceback yourself `traceback.format_exc()` returns the same text as a string, and `log.error("failed: " + traceback.format_exc())` looks equivalent. It is worse in three ways. The traceback becomes part of the message string, so structured handlers cannot separate it; anything that filters, redacts or ships records field-by-field sees one opaque blob. Downstream formatters lose the ability to decide whether to render it at all. And, called outside a handler, `format_exc()` quietly yields the string `"NoneType: None"` and concatenates it into your message rather than being visibly absent. Let logging carry the exception as data and let the handler decide how to render it. ## Writing the message The traceback already tells you the exception type, its message and the code path. It does not know which of 6,800 rows you were on, which remote host you called, or which tenant the request belonged to. So the message should carry the *identifying* context and not restate the exception: `log.exception("inventory sync failed for batch %s at row %d", batch_id, offset)` beats `log.exception("got a TimeoutError")`. Use `%`-style placeholders with arguments rather than an f-string so the record keeps the parameters separately and the formatting cost is skipped when the level is disabled. ## The decision it implies Calling `log.exception()` is a claim: *this failure stops here, and this record is its report.* If you are going to re-raise, the exception is still travelling with its traceback intact and something above you will report it — logging here just doubles the report. That is the boundary rule this leaf is really about, and `logging.exception()` is the tool you use exactly once, at the layer that actually stops the failure.

  • What appears in the log if logging.exception() is called outside any except block?
    The message, followed by the line `NoneType: None`. `exc_info=True` captures the currently handled exception, and outside a handler there is none, so the record carries an empty `(None, None, None)` triple that the formatter renders that way. Seeing `NoneType: None` in production logs is a reliable signal that someone logged a traceback where no exception was live — often because the `except` block had already finished.
  • How would you attach a traceback to a WARNING rather than an ERROR?
    Pass `exc_info` to the level method directly: `log.warning("row deferred", exc_info=True)` inside the handler, or `log.warning("row deferred", exc_info=exc)` if you hold the exception object. `logging.exception()` is hardwired to ERROR, so using it just to get a traceback misrepresents severity — a failure you deliberately absorb and retry is a warning, not an error.
  • Why prefer log.exception('failed for %s', key) over an f-string in the message?
    With `%`-style placeholders the record keeps the message template and the arguments as separate fields, so aggregators can group thousands of records under one template, and the interpolation is skipped entirely when the level is disabled. An f-string is formatted eagerly at the call site and yields a unique string per record, which defeats grouping.

saying these in an interview costs you the question

  • Thinking logging.error() includes the traceback by default
  • Calling logging.exception() outside an except block
  • Believing exc_info only accepts True
  • Confusing stack_info with exc_info
  • Concatenating traceback.format_exc() into the message
  • Using logging.exception() purely to get a traceback at ERROR

context