Why does `str(exc)` on a caught `NameError` omit the "Did you mean" hint the console prints?
answer
- The terminal shows more than the message
- Nothing is added when the error is raised
- Ask when the hint is computed
- The renderer searches for candidate names
- Same reason the carets are missing
basics
~20 sThe suggestion is computed when the exception is displayed, not when it is raised. The message string stays "name 'x' is not defined"; the traceback machinery adds the closest matching name it can find in the failing frame.
solid answer
~40 sThe hint is a **display-time** enrichment, exactly like the caret anchors. When an exception is being rendered, the traceback machinery takes the name that failed - the exception object carries it - and searches the candidates available where it was raised: the frame's local, global and builtin names for a `NameError`, the object's attribute names for an `AttributeError`, the module's contents for `ImportError`. If one candidate is close enough by a bounded similarity search, it is appended to the printed message. None of that touches the exception's own message, so `str(exc)` and any log line built from it lose the hint. Log the rendered traceback if you want it. Suggestions arrived in **Python 3.10** for `NameError` and `AttributeError`; **3.12** added `from ... import ...` suggestions and the `self.attr` hint inside methods.
code
python · 6 linestry:
lenght = 3
print(lenghth)
except NameError as exc:
print(repr(str(exc)))
print(exc.name)go deeper
Recognise that the "Did you mean" line comes from the interpreter itself, not your editor, and that it appears when an error is printed. Use it as the first thing to read on a NameError.
Explain the split: the exception carries the failed name, and the display machinery searches the frame's names or the object's attributes for a close match. That is why str(exc) lacks the hint.
Connect it to logging practice. A service that logs only formatted messages silently drops suggestions, caret anchors and chained causes; know why, and know that display-time enrichment makes the rendered sentence a poor grouping key in an aggregator.
Set the expectation that terminal diagnosability and production diagnosability are not the same thing, and require a test that a real crash reaches the log store with its full rendered form rather than a stringified message.
Type `print(lenghth)` at a Python prompt after binding `lenght` and you get: ``` NameError: name 'lenghth' is not defined. Did you mean: 'lenght'? ``` Catch the same error and print `str(exc)` and you get `name 'lenghth' is not defined` - no hint. That difference is not a bug, and understanding it explains a whole family of "my logs are worse than my terminal" complaints. ### Two different strings An exception instance holds an argument tuple, and `str()` on it renders that. For `NameError` the interpreter fills it in at raise time with the plain sentence. The suggestion is produced **later, by the code that renders a traceback**, and only if a traceback is actually rendered. Since most exceptions in a running system are caught and handled, doing the search at raise time would tax every handled error to benefit the small minority that get printed. Deferring it is the right design, and it has the consequence above. ### What the search actually looks at The exception object carries the failing name, which is why a suggestion is possible at all: the renderer does not have to re-parse source. It then needs candidates, and where it gets them depends on the exception: - **`NameError`** - the names visible in the frame that raised: its locals, its globals, and the builtins. - **`AttributeError`** - the attribute names of the object that was accessed, essentially what `dir()` would report. - **`ImportError`** from a `from ... import ...` statement - the names the module actually exports. Each candidate is compared to the failing name with a cheap edit-distance style similarity, and the best match is offered only if it is close enough. The search is deliberately bounded: when the candidate set is very large, or the names are long, CPython skips the search rather than spend real time inside error display. So the absence of a suggestion tells you nothing about whether a near-miss name exists - a typo inside a module with thousands of globals may simply not be searched. This also explains a gap people notice: an attribute served dynamically by a `__getattr__` implementation, which never appears in `dir()`, cannot be suggested. The renderer can only propose names it can enumerate. ### The `self` case **Python 3.12** added a suggestion that reads differently from the rest. Inside a method, if you write `duration` where you meant `self.duration`, the `NameError` display says `Did you mean: 'self.duration'?`. It is the same machinery, extended with the instance's attributes as candidates when the failing frame belongs to a method. It catches one of the most common Python-in-a-class mistakes, and it is worth knowing it exists because it is the kind of hint an engineer reading only log messages will never see. ### Why this matters beyond the REPL A great deal of production Python logs errors as a single formatted message. Written that way, three things the interpreter computed for you are discarded: the caret anchor showing which sub-expression failed, the chained context of a `raise ... from ...`, and this suggestion. The fix is not clever - render the exception rather than stringifying it, and send the rendered text to the log - but teams routinely discover it only after an incident where the terminal reproduction was obviously diagnosable and the production log was not. There is a second, quieter consequence. Because the suggestion is generated at display time from live namespaces, the text of a printed error can differ between two runs of the same code - a name defined by one code path and not another changes the candidate set. That is fine for a human reading a terminal, and mildly annoying if you are grouping errors by exact message in an aggregator: the same defect can produce two message strings. Group on the exception type and the code location, not on the rendered sentence. ### Version summary Suggestions for `NameError` and `AttributeError` arrived in **Python 3.10**. **Python 3.12** extended them to `from ... import ...` names and added the `self.attr` form. Nothing about them is stored in the exception message in any of these versions, which is the part that stays true across releases.
- When does Python decline to offer a suggestion at all?When nothing is close enough, and when the search would be expensive: CPython bounds it, skipping the comparison if the candidate namespace is very large or the names are long. It runs while an error is being printed, so it is deliberately cheap. The absence of a hint therefore does not mean no similar name exists.
- Why can an attribute served by `__getattr__` never be suggested?Because the renderer needs to enumerate candidates, and it uses the attribute names the object reports - essentially what `dir()` would give. A name that is synthesised on demand by `__getattr__` and appears in no such listing is invisible to the search, so an `AttributeError` for it gets no hint even when the spelling is one character off.
- How should a service log errors so the hints and anchors survive?Log the rendered exception, not `str(exc)`. Stringifying keeps only the message the exception was constructed with, discarding the caret anchor, the chained cause and the suggestion. Group errors in an aggregator by exception type and code location rather than by the rendered sentence, since display-time enrichment can vary between runs.
saying these in an interview costs you the question
- Thinks the suggestion is part of the exception's message string
- Says a third-party error formatter produces the hint
- Assumes a suggestion always appears for a near-miss typo
- Believes the hint is computed when the exception is raised
- Confuses the runtime hint with a linter or type-checker warning