skip to content

Annotation Syntax and Forward References

Where annotations go, how Python stores them, and why a type you have not defined yet has to be quoted. Interviewers ask it to check you know annotations are metadata, not runtime enforcement.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

Are Python type annotations enforced at runtime, and what does the interpreter do with them?

level: juniorimportance: must knowfreq 75%

answer

  1. Metadata, not a guarantee
  2. The interpreter only records them
  3. Look for the dict they land in
  4. __annotations__ on function, class, module
  5. A checker or a library must read them

basics

~20 s

No. CPython never compares a value against its annotation. It only records annotations as metadata in the annotations dictionary on functions, classes and modules; enforcement comes from a separate static type checker or from library code that reads them.

solid answer

~50 s

Annotations are metadata, not checks. `def charge(amount: float) -> bool` validates nothing: `charge("free")` runs happily and returns whatever the body returns. CPython simply records what you wrote in `__annotations__` — a dict on the function keyed by parameter name plus `'return'`, and a dict on a class or module keyed by the annotated variable name. Everything that acts on annotations is a separate consumer: a static type checker run in CI, an editor, or runtime library code such as `dataclasses.dataclass`, which reads a class's annotations to generate an `__init__` and still does not validate the values passed to it. So an annotation is a claim that some tool has to be run to verify. If no checker runs, a wrong annotation is silent, confident documentation, and the program behaves exactly as it would with no annotations at all.

code

python · 5 lines
python
def charge(amount: float, retries: int = 3) -> bool:
    return "not a bool"

print(charge("free", retries=None))
print(charge.__annotations__)

go deeper

for a junior

Be ready to say plainly that annotations are not checked when the code runs, and to name the dictionary they are stored in. Knowing that a wrong annotation causes no error at all is the whole of the screening answer here.

for a middle

Explain the mechanics: which object holds __annotations__, what the keys are for parameters and the return, and which stdlib machinery reads annotations to build code without validating values. Be able to show that any expression is accepted.

for a senior

Demonstrate the production judgment: annotations plus a checker in CI cover the interior of a system, while data crossing a boundary needs explicit conversion or validation. Be able to describe a bug that annotations documented and did not prevent.

for a principal

Own the policy question: what checker strictness a codebase adopts, whether checking gates merges, where runtime validation is mandatory, and what a partially annotated legacy codebase costs. Argue why a strict setting everywhere is not automatically the right call.

## What the syntax actually does Python lets you attach an expression to three things: a function parameter (`def charge(amount: float)`), a function's return (`-> bool`), and a variable binding (`rate: float = 0.2`). The interpreter treats that expression as **data about the code**, never as a constraint on it. There is no hidden `isinstance` call at the call boundary, no coercion, no warning. Passing a `str` where the annotation says `float` is not an error to CPython; it is an error only to a tool you chose to run. ## Where it is stored Function annotations land in the function object's `__annotations__` dict, keyed by parameter name, with the return annotation under the key `'return'`. Class-body and module-level variable annotations land in the class's or module's `__annotations__` dict, keyed by variable name. That dict is an ordinary mutable dict of ordinary objects — you can read it, and you can write to it. ```python def charge(amount: float, retries: int = 3) -> bool: return "not a bool" charge.__annotations__ # {'amount': <class 'float'>, 'retries': <class 'int'>, 'return': <class 'bool'>} ``` An annotation may be *any* expression, not only a type: `def f(x: "anything at all"): ...` is legal, and so is `def f(x: 1 + 1): ...`. The language does not care; a type checker does. ## Who does the enforcing Four kinds of consumer, and it is worth naming them separately in an interview: 1. **Static type checkers.** They read your source (not the running program) and report mismatches. They run in CI or your editor. If you never run one, annotations change nothing. 2. **Editors and tooling.** Completion, go-to-definition, refactoring accuracy. 3. **Stdlib consumers.** `dataclasses.dataclass` decides which class attributes are fields by reading the class's annotations; `typing.NamedTuple` does the same. Note what they do *not* do: a dataclass will happily store a `str` in a field annotated `float`. 4. **Runtime validation libraries.** A third-party validation library can build real validators from a model class's annotations — but the validation comes from that library's code, not from Python. ## Why interviewers ask it Because the failure mode is quiet and expensive. Take a subscription-billing job that runs for six hours every night. Some helper is annotated `-> Decimal` but a refactor made it return a `float`; a later step divides and rounds, and the totals drift by fractions of a cent across hundreds of thousands of line items. Nothing raised. The annotation was true when it was written and false when it shipped, and only a checker in CI, or an explicit conversion or validation at the boundary where the value enters the system, would have caught it. A candidate who believes the annotation itself is the guard will not add either. ## The practical corollary If you want a runtime failure, you must write one: convert or validate explicitly at the edges of your program (parsing input, reading a database row, receiving a payload) and let annotations plus a checker cover the interior. That split — static checking inside, real validation at the boundary — is the answer an interviewer is listening for. ## One version note Non-enforcement has been true since annotations existed, and variable annotations arrived in Python 3.6. What changed in **3.14** is only *when* the annotation expression is evaluated: annotations are now computed lazily, on first access to `__annotations__`, instead of at definition time. The dict you get back is the same, and enforcement is still nobody's job but yours.

  • If nothing enforces them, what actually consumes annotations in a real project?
    A static type checker in CI and in the editor, first of all. Then stdlib machinery that reads a class's annotations to build code — `dataclasses.dataclass` and `typing.NamedTuple`. Then third-party validation libraries that generate real validators from a model class's annotations. And your own introspection code, if you read `__annotations__` directly. Each of those is opt-in; none of them is the interpreter.
  • Can an annotation be any expression, or must it be a type?
    Syntactically it can be any expression — `def f(x: 1 + 1): ...` is valid Python, and a string, a call or a list literal are all accepted. The interpreter only records the expression; only a type checker insists that it denote a type, and it will reject nonsense that the interpreter waves through.
  • How would you get an actual runtime error when a caller passes the wrong type?
    Write the check. Convert or validate explicitly where untrusted data enters — parsing input, reading a row, unpacking a payload — with an `isinstance` check, a constructor call that raises on bad input, or a validation library that builds validators from the annotations. Annotations plus a checker cover the interior of the program; they never cover the boundary.

An annotation is a label on a shipping crate. It tells everyone what is supposed to be inside, and the warehouse will still ship the crate if someone packed the wrong thing; only an inspector who actually opens it finds out.

saying these in an interview costs you the question

  • Says Python raises TypeError when an argument does not match its annotation
  • Thinks annotations make code faster or change how it compiles
  • Believes a type checker runs automatically as part of the interpreter
  • Cannot say where the interpreter stores the annotations
  • Assumes dataclass field annotations validate the values assigned
  • Treats an annotation as a substitute for validating untrusted input

context

open as a page

Why is a forward reference in a Python annotation written as a quoted string?

level: middleimportance: should knowfreq 55%

basics

~20 s

A 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.

open as a page

Where does Python store a variable annotation written inside a function body?

level: middleimportance: should knowfreq 32%

basics

~20 s

Nowhere. An annotation on a local variable is neither evaluated nor recorded, so only a static checker ever sees it. Module-level and class-body annotations are different: those are stored in that module's or class's annotations dictionary.

open as a page

How do you write Python annotations that reference a class in a module you cannot import at runtime?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Guard the import with an if TYPE_CHECKING: block, which type checkers follow but the interpreter never executes, and write the annotation as a quoted string so nothing tries to resolve the missing name while the program runs.

open as a page