Are Python type annotations enforced at runtime, and what does the interpreter do with them?
answer
- Metadata, not a guarantee
- The interpreter only records them
- Look for the dict they land in
- __annotations__ on function, class, module
- A checker or a library must read them
basics
~20 sNo. 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 sAnnotations 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 linesdef charge(amount: float, retries: int = 3) -> bool:
return "not a bool"
print(charge("free", retries=None))
print(charge.__annotations__)go deeper
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.
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.
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.
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