Why is a forward reference in a Python annotation written as a quoted string?
answer
- The name does not exist yet
- Quotes buy the lookup some time
- The string survives into storage
- A checker reads it as source anyway
- 3.13 evaluated annotations at definition time
basics
~20 sA quoted annotation is stored as the plain string, so the name inside it need not exist when the line runs. Through Python 3.13 annotations were evaluated at definition time, so a class defined later had to be quoted.
solid answer
~50 sWriting `other: "Ledger"` puts the *string* into `__annotations__` instead of looking the name up. That matters because a name has to be bound before an expression that mentions it is evaluated, and the classic cases break that rule: a method whose annotation names the class currently being defined, or a function annotated with a class that appears later in the file. Through **Python 3.13**, annotations were evaluated as the `def` or the class body executed, so those raised `NameError` unless quoted. A static type checker parses the quoted text as if you had written it unquoted, so nothing is lost. From **3.14** annotations are evaluated lazily, so the self-reference case works unquoted — but strings are still the tool when the name will never be bound at runtime, and when the file must import on older interpreters.
code
python · 5 linesclass Ledger:
def merge(self, other: "Ledger") -> "Ledger":
return self
print(Ledger.merge.__annotations__)go deeper
Recall the shape and the reason: a type name in quotes is a forward reference, used when the class is not defined yet. Recognising one in a codebase and not being alarmed by it is enough at this level.
Explain the mechanics: the string is stored verbatim in __annotations__, a checker parses it as if unquoted, and the class name is unbound inside its own body until the class statement completes. Name the version that changed the timing.
Show judgment about when a string is still the right call — names that never exist at runtime, files that must import on older interpreters, code that introspects annotations — and be able to predict whether a given file breaks on 3.13 but not on 3.14.
Own the convention: whether a codebase quotes forward references uniformly, what minimum interpreter version it supports, and how that choice interacts with libraries that read annotations at runtime. Justify the rule you would write down for a team.
## The problem quoting solves An annotation is an expression, and an expression that mentions a name needs that name to be bound by the time it is evaluated. Two everyday situations violate that: - A method that returns its own class. Inside `class Ledger:`, the name `Ledger` is not bound until the whole `class` statement finishes, so `def merge(self, other: Ledger) -> Ledger` mentions a name that does not exist yet. - A function annotated with a class defined further down the module, or with a class that only a type checker will ever see. Quoting the annotation — `other: "Ledger"` — sidesteps the lookup entirely. A string literal is just a string: there is nothing to resolve, so nothing can fail. ## What ends up stored The string is stored verbatim. It is not parsed, not resolved, not converted into the class object: ```python class Ledger: def merge(self, other: "Ledger") -> "Ledger": return self Ledger.merge.__annotations__ # {'other': 'Ledger', 'return': 'Ledger'} ``` That is the single most important detail to state in an interview: **a quoted annotation stays a string in `__annotations__`**. Turning it into the class object is a separate, explicit step that some tool has to perform. ## What a type checker sees A checker treats `x: "Ledger"` and `x: Ledger` identically. It parses the contents of the string as an expression and resolves it in the enclosing scope, using the whole module — including definitions that appear after the annotated line, which is why a forward reference is legal to a checker in the first place. A consequence worth knowing: a typo inside the quotes is still an error. Quotes hide the name from the *interpreter*, not from the checker. ## The version story, stated precisely Through **Python 3.13**, annotations were evaluated eagerly: the expressions ran while the `def` statement or the class body executed, so an unquoted forward reference raised `NameError` on import. Quoting was the fix, and `from __future__ import annotations` was the file-wide alternative. In **Python 3.14**, annotations are no longer evaluated at definition time; they are computed lazily, when something reads `__annotations__`. The practical effect on this material is direct: ```python class Node: children: list[Node] def clone(self) -> Node: return self ``` On 3.14 this class body executes fine unquoted, and reading `Node.clone.__annotations__` afterwards yields the real class object, because by then `Node` is bound. On 3.13 the same code raises `NameError` while the class body runs. ## So are quotes obsolete? No, for three reasons, and the interviewer usually wants at least two of them: 1. **Names that never exist at runtime.** If a class is imported only for the checker's benefit, and that import never executes, no amount of deferred evaluation will resolve it — reading the annotations raises `NameError` at that moment. A string is never read as a name at all. 2. **Older interpreters.** A library that must import on 3.13 or earlier still needs quotes for any forward reference. 3. **Keeping runtime consumers honest.** If a decorator or a runtime tool walks a class's annotations, a string tells you plainly that nothing has been resolved yet. ## Common mistakes Quoting only part of an annotation and getting the nesting wrong — `list["Ledger"]` is fine, `"list[Ledger]"` is also fine, mixing them inconsistently is merely noisy. Believing that the interpreter substitutes the class in later — it does not; the string stays a string. And assuming quotes silence the checker — they do not; a misspelled class name inside quotes is reported exactly as it would be unquoted.
- How does a static type checker treat `x: "Ledger"` differently from `x: Ledger`?It does not treat them differently. The checker parses the text inside the quotes as an expression and resolves it in the enclosing scope, considering the whole module, so a forward reference is legal and a misspelling inside the quotes is reported exactly as it would be without them. Quotes hide the name from the interpreter, never from the checker.
- What ends up in `__annotations__` for a quoted annotation?The string, unchanged. Reading `Ledger.merge.__annotations__` for `other: "Ledger"` gives `{'other': 'Ledger', ...}` — not the class object. Converting that text into an object is a separate, explicit step; nothing in the interpreter does it for you, which is why code that introspects annotations must be prepared to find strings.
- Is a quoted forward reference still needed on Python 3.14?Not for the common case of naming a class defined later in the same module, since annotations are no longer evaluated at definition time and the name is bound by the time anything reads them. Quotes are still needed for a name that is never bound at runtime, and in any file that must also import on Python 3.13 or older.
A quoted annotation is like writing a colleague's name on an envelope instead of handing them the letter: the name is recorded now, and finding the person is somebody else's job, later.
saying these in an interview costs you the question
- Thinks the quotes around an annotation are a style preference
- Says the interpreter later replaces the string with the class object
- Cannot explain why a class name is unbound inside its own body
- Believes a typo inside the quotes is invisible to a type checker
- Unaware that Python 3.14 evaluates annotations lazily
- Claims quoting changes what the annotated code does at runtime