What does BaseException.add_note() do to an exception object in Python?
answer
- Extra context without a new exception type
- Python 3.11, PEP 678
- A method on BaseException itself
- Appends to a list on the instance
- __notes__ prints below the error line
basics
~20 sadd_note appends a string to the exception instance's notes list without touching its class, args or message. Python 3.11 added it. Each note is printed on its own line just below the traceback's final error line.
solid answer
~40 s`add_note` is defined on `BaseException`, so every exception has had it since Python 3.11 (PEP 678). Calling `exc.add_note("record_id=8842")` creates `exc.__notes__` on first use and appends the string to that list — the attribute simply does not exist until then, so read it with `getattr(exc, "__notes__", [])`. The exception's class, `args` and `str(exc)` are unchanged: a note is human-readable context, not payload. Whatever renders the traceback — the default handler, `traceback.format_exception`, or logging at exception level — prints each note on its own line under the final `ValueError: message` line, in the order the notes were added. The argument must be a `str`; anything else raises `TypeError`. The point is to enrich an error that already exists rather than replace it with a new one.
code
python · 6 lineserr = ValueError("score out of range")
print(hasattr(err, "__notes__")) # False - created on first add_note
err.add_note("record_id=8842")
err.add_note("scorer=v3")
print(err.__notes__) # ['record_id=8842', 'scorer=v3']
print(str(err), err.args) # message and args are unchangedgo deeper
Recall the one-liner: add_note attaches a string to an existing exception and it shows up under the traceback's last line. Be able to say it does not change the type or the message.
Explain the mechanics: the method lives on BaseException, notes is created lazily on the first call, notes append in order, and only a str is accepted. Know which renderers print them.
Show judgement about content: short key=value identifiers, never secrets or payloads, and an attribute rather than a note whenever code must branch on the value. Know that str(exc)-based logging loses notes entirely.
Own the convention across services: what every layer is expected to note, how it reaches your error aggregator intact, and where the team should use structured exception fields instead of free text.
`add_note` is a method on `BaseException` itself, which means **every** exception object in Python carries it: a `ValueError`, your own subclass, `KeyboardInterrupt`, an `ExceptionGroup`. It arrived in Python 3.11 with PEP 678 and is unchanged through 3.14. Its entire job is to attach an extra line of human-readable context to an exception *instance that already exists*, without constructing a new exception and without altering the one in hand. ## What the call actually does `exc.add_note("record_id=8842")` looks for a list attribute named `__notes__` on the instance; if there is none it creates one, then appends the string. Two consequences fall straight out of that implementation: * **The attribute is absent until the first call.** `ValueError("x").__notes__` raises `AttributeError`. Any code that inspects notes generically must use `getattr(exc, "__notes__", [])`, and that is the single most common bug when people first automate around this feature. * **Notes accumulate, they do not replace.** A second call appends, and the list preserves call order. That is what makes a trail of notes readable as a path from the innermost layer outward: the first line is where the failure happened, the last is the outermost frame that decorated it. Because `__notes__` is an ordinary list on an ordinary object, you can also read it, filter it, or replace it wholesale. There is no hidden machinery. ## What it deliberately does not touch The exception's class is the same, so every `except` clause downstream behaves identically. `args` is the same, so anything constructing a message from `exc.args` is unaffected. `str(exc)` is the same, which matters more than it sounds: a log line written as `logger.error("failed: %s", exc)` prints only the message and silently drops every note, while anything that formats the **full traceback** includes them. The traceback object and any chaining links are untouched as well. That separation is the design: a note is metadata for a human reading a failure, not data for a program. If some caller must branch on the context you are adding, a note is the wrong carrier — set an attribute on the exception instead. If a human debugging a production traceback needs to know which record, which shard, which config file, a note is exactly right. ## The argument is a string, always Passing anything else raises `TypeError: add_note() argument must be str, not bytes`. Format the value yourself: ```python exc.add_note(f"record_id={record_id}") ``` There is no implicit `str()` conversion and no multi-argument form; call `add_note` once per line you want. ## How notes render Every renderer in the standard library goes through the same formatting code, so notes show up consistently: the default interpreter handler on an uncaught exception, `traceback.print_exception`, `traceback.format_exception`, `traceback.format_exc`, and `logging.Logger.exception`. The shape is: ``` Traceback (most recent call last): ...frames... ValueError: score out of range record_id=8842 scorer=v3 ``` Each note occupies its own line, unindented, directly below the `Type: message` line and in insertion order. `traceback.TracebackException` captures the notes too, so an exception summarised now and formatted later — or serialised and rendered elsewhere — keeps them. If someone has assigned a non-list to `__notes__`, the formatter does not crash; it falls back to printing that object's repr. ## Why the feature exists Before 3.11 there were three ways to add context to an error travelling up a stack, and each cost something. Build a new exception with a richer message: now callers must catch a different type, and readers must follow a chain. Mutate `args`: fragile, and downstream code may depend on its shape. Log at every layer: one failure becomes several log lines that only help if your log pipeline reassembles them. `add_note` is a fourth option that preserves identity — the same object, the same type, the same traceback, plus a line of context. ## Typical shape The two common patterns are annotating an exception you are about to raise, and annotating one you caught. The first looks like: ```python err = ValueError("score out of range") err.add_note("record_id=8842") raise err ``` You need an instance to call the method on, so this splits the usual one-line `raise ValueError(...)`. The second — catch, annotate, re-raise the same object — is the pattern most interviews are really probing, and it is what makes notes accumulate across layers. ## Content guidance Keep notes short, greppable and key=value shaped: identifiers, stage names, versions, the config path that was loaded. Never put credentials, tokens, personal data or a large payload in one, because a note is reproduced verbatim everywhere the traceback goes — crash reports, aggregators, tickets pasted into chat.
- What happens if you pass something other than a str to add_note?It raises `TypeError: add_note() argument must be str, not bytes` — with whatever type you passed named. There is no implicit conversion, so format the value first, as in `exc.add_note(f"record_id={record_id}")`. If a program rather than a person needs the value, set an attribute on the exception instead of encoding it in a note.
- Do notes change what str(exc) or exc.args returns?No. Notes live only in `exc.__notes__`. `args`, `str(exc)` and the exception's class are untouched, which is why a log line built from `str(exc)` alone silently drops every note, while anything formatting the full traceback — the default handler, `traceback.format_exception`, `logging.Logger.exception` — includes them.
- Can you read or edit the notes list directly?Yes. Once it exists, `__notes__` is an ordinary list attribute you can read, filter or replace; there is no private machinery behind it. Just remember it is absent until the first `add_note` call, so read it as `getattr(exc, "__notes__", [])` rather than assuming an empty list is there.
Notes are sticky labels stuck on an envelope as it is passed from desk to desk: the letter inside — the exception's type and message — is untouched, but every desk can add a line the final reader will see.
saying these in an interview costs you the question
- Says add_note changes the exception's message or args
- Assumes __notes__ exists on every exception from the start
- Passes a dict or an int to add_note
- Thinks a second note replaces the first
- Expects a log line built from str(exc) to show the notes
- Calls it a Python 2 or 3.8 era feature