skip to content

How do you use add_note in an except handler to add context before re-raising?

level: middleimportance: should knowfreq 28%

answer

  1. Enrich the error, do not replace it
  2. Bind it, annotate it, re-raise it
  3. The as-name disappears after the block
  4. Bare raise keeps the original traceback
  5. Notes stack inner layer to outer

basics

~20 s

Catch the error with except ... as exc, call exc.add_note with what this layer knows, then use a bare raise. The same object keeps propagating with its type, args and traceback intact, now carrying an extra line of context.

solid answer

~40 s

The idiom is three lines: `except UnicodeDecodeError as exc:`, `exc.add_note(f"record_id={record_id}")`, then a bare `raise`. Mutating the caught exception is safe because the handler holds the very object that will continue up the stack, so the note travels with it; a bare `raise` re-raises that object without adding a frame for the raise statement itself. Every layer that catches it can append its own note, and the notes stack in inner-to-outer order — a breadcrumb trail in the final traceback. Do the annotation **inside** the `except` block: the `as` name is unbound when the block ends. This is the alternative to wrapping the error in a new type, which changes what callers must catch; reach for a wrapper only when you actually want the contract to change.

code

python · 12 lines
python
def load(record_id: int, raw: bytes) -> str:
    try:
        return raw.decode("utf-8")
    except UnicodeDecodeError as exc:
        exc.add_note(f"record_id={record_id}")
        raise


try:
    load(8842, b"\xff\xfe")
except UnicodeDecodeError as exc:
    print(exc.__notes__)   # ['record_id=8842']

go deeper

for a junior

Recall the shape: except ... as exc, exc.add_note("..."), then a bare raise. Know that the exception keeps its original type, so callers above catch it exactly as before.

for a middle

Explain why mutating the caught object is safe, that the as-name is unbound at the end of the block, and how notes from several layers stack in inner-to-outer order in the final traceback.

for a senior

Show the operational judgement: what belongs in a note versus an attribute, why secrets and payloads never go in one, and when a wrapper type is the right call because the caller's contract really should change.

for a principal

Own the convention: which layers annotate, what identifiers every service is expected to attach, and how that free text relates to the structured fields your error tracking actually indexes.

The pattern interviews are really asking about is *enriching an error in flight*. A failure happens deep in a call stack, where the code knows the mechanism but not the business context; the layers above know the context — which record, which shard, which tenant — but not the mechanism. `add_note` lets each layer contribute what it knows to one exception object. ## The idiom ```python def load(record_id: int, raw: bytes) -> str: try: return raw.decode("utf-8") except UnicodeDecodeError as exc: exc.add_note(f"record_id={record_id}") raise ``` Three things make this work. The `as exc` binding is the **same object** that will keep propagating, so mutating it is not a copy-and-lose situation. `add_note` appends rather than replaces, so an outer layer can add `batch=night-rescore` on top of the inner `record_id=8842`. And the bare `raise` re-raises the exception currently being handled, preserving its class and traceback so every `except` clause above behaves exactly as it would have without your handler. ## Bare raise versus raise exc Both re-raise the same object and both carry the notes. The difference is cosmetic but visible: `raise exc` adds a traceback entry pointing at your `raise` line, so the rendered traceback shows the re-raise site before the original frames; a bare `raise` does not. Bare `raise` is the idiom, and it cannot accidentally raise the wrong name. ## The `as` name is deleted Python unbinds the `except ... as exc` name at the end of the block — it is compiled to an implicit `del exc` — so touching `exc` afterwards raises `NameError` (an `UnboundLocalError` inside a function). Annotate inside the block. If you genuinely need the object later, bind it to another name first (`err = exc`) before the block ends. ## Why not just wrap it Wrapping — catching a low-level error and raising your own type instead — is a contract change: callers who used to catch the original type now cannot, and readers of the traceback have a second exception to walk. That is sometimes exactly what you want, when the low-level type leaks an implementation detail your API should not promise. But when the type is right and only the *context* is missing, wrapping is a heavy tool. `add_note` adds the context and leaves the contract alone. The other alternative — logging at each layer — turns one failure into several log lines that only reassemble if your pipeline correlates them. A note travels attached to the object, so wherever the traceback is finally rendered, the whole trail is in one place. ## What to put in a note Consider a fraud-scoring service that re-scores a 2.4 GB working set of stored records overnight and hits an encoding mismatch on a handful of them. Useful notes are the record identifier, the shard or batch name, the codec that was attempted, the stage of the pipeline. Useless or dangerous ones are the record itself (it may be large, and a note is a string held for the lifetime of the exception), anything containing credentials or personal data, and anything a program is expected to parse back out. Notes are reproduced verbatim in every rendered traceback, every crash report and every ticket someone pastes into chat, so treat them as public and keep them short and greppable. ## Pitfalls **A reused exception instance accumulates notes forever.** If a module-level exception object is raised repeatedly — a pre-built sentinel, or an exception cached and re-raised — every annotation piles onto the same `__notes__` list, and the tenth failure shows ten stale lines. Build a fresh exception per failure, which is the norm anyway. **A retry loop double-annotates.** Annotating the same caught exception on each of three attempts leaves three near-identical notes. Add the attempt number if you want that, or annotate once at the boundary. **Notes are not a data channel.** Because `add_note` takes a string and nothing enforces its shape, it is tempting to encode structured context and parse it back in an outer handler. That is a fragile contract with no schema; if code must branch on the value, put it on the exception as an attribute and let the note carry the human-readable rendering. **Silent loss in string-formatted logs.** `logger.error("failed: %s", exc)` prints only the message. Use `logger.exception(...)` or format the traceback if you want the notes to reach the log. ## In review The pattern is small enough to spot in a diff: a handler that binds the exception, adds one short note, and re-raises bare. Anything that catches, logs, and swallows — or catches and re-raises a different type without a reason — is the thing to question.

  • Why re-raise with a bare raise rather than raise exc?
    Both re-raise the same object with its notes, but `raise exc` adds a traceback entry for your raise statement, so the rendered traceback shows the re-raise site above the original frames. A bare `raise` re-raises the exception currently being handled without that extra line, and it cannot pick up the wrong name by accident.
  • When should you wrap the error in a new exception type instead of adding a note?
    When the caller's contract should change — the low-level type leaks an implementation detail your API does not want to promise, or callers genuinely need to handle it as your domain error. If callers should still catch the original type and only the context is missing, a note is the lighter and more honest tool.
  • Where does a note go if you annotate a caught exception but do not re-raise it?
    Nowhere useful. The note is on the object; if the handler swallows the exception, the object is discarded and the note dies with it. Annotate as part of re-raising, or, if you are handling the error, log the formatted traceback so the notes are rendered before you drop the exception.

saying these in an interview costs you the question

  • Uses the as-name after the except block ends
  • Wraps every error in a new type just to add context
  • Puts payloads, tokens or personal data in a note
  • Annotates a module-level exception reused across calls
  • Parses note text back out for control flow
  • Thinks add_note itself stops the exception propagating

context