Where does Python store a variable annotation written inside a function body?
answer
- Three scopes do three different things
- One of them keeps nothing at all
- Not evaluated means a bogus name is fine
- Class body records it without an attribute
- Function __annotations__ holds parameters only
basics
~20 sNowhere. 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.
solid answer
~40 sVariable annotations behave differently in three scopes. In a **function body**, `rate: float = 0.2` records nothing: the annotation expression is not even evaluated, so an undefined name inside it never raises, and the function's `__annotations__` holds only its parameters and return. In a **class body**, `total: float` is stored in the class's `__annotations__` — but it creates no attribute, so `Invoice.total` raises `AttributeError` until something assigns a value. At **module level** the annotation goes into the module's `__annotations__`, reachable through the module object. One extra consequence in a function: even a bare `total: int` with no value marks that name as local for the whole function, so reading it before assignment raises `UnboundLocalError` instead of falling back to a global.
code
python · 6 linesdef run_billing():
rate: Undefined = 0.2
return rate
print(run_billing())
print(run_billing.__annotations__)go deeper
Recall that a function's __annotations__ covers its parameters and return, and that annotating a local variable is purely a note for tooling. Do not expect a dictionary of local annotations to exist.
Explain all three scopes and both surprises: a local annotation is not evaluated at all, and a class-body annotation without a value records the name while creating no attribute. Be able to demonstrate each from the interpreter.
Connect the storage rules to the tools that depend on them — record-building decorators reading a class's annotations for field names and reading class attributes for defaults — and explain why storing local annotations would be cost without a consumer.
Own the convention around annotating module-level state and class attributes when other machinery introspects it, and be able to argue what a codebase should rely on at runtime versus what it should leave entirely to static checking.
## Three scopes, three behaviours PEP 526 gave Python variable annotations — `name: SomeType` with or without a value — and the storage rules differ by where you write them. Interviewers use this to check that you know annotations are a compile-time artefact whose runtime footprint is deliberately small. ### Function locals: not stored, not even evaluated ```python def run_billing(): rate: Undefined = 0.2 return rate run_billing() # 0.2 — no error run_billing.__annotations__ # {} — parameters and return only ``` `Undefined` is not a name that exists anywhere, and nothing complains, because the annotation on a local variable is never evaluated. There is no dict of local annotations either — not on the function, not on the frame. The information exists in the source for a type checker to read, and nowhere else at runtime. There is one runtime effect, and it is the part candidates miss. An annotated name is still a *binding target* as far as the compiler is concerned, so it becomes local to the function even with no value assigned: ```python total = 100 def report(): total: int # no value — but `total` is now local return total # UnboundLocalError, not 100 ``` ### Class bodies: stored, but no attribute is created ```python class Invoice: total: float currency: str = "EUR" Invoice.__annotations__ # {'total': float, 'currency': str} Invoice.total # AttributeError Invoice.currency # 'EUR' ``` The annotation is recorded; the attribute is created only by the assignment. This is exactly the distinction that class-building machinery relies on: a decorator that turns a class into a record type reads the annotations to learn the field names and their order, and reads the class attributes to learn which fields have defaults. A bare `total: float` therefore means *a required field*, and `currency: str = "EUR"` means *a field with a default* — and none of that is enforcement, only construction. ### Module level: stored on the module A module-level `due: float` goes into the module's `__annotations__` dict. Read it through the module object: ```python import sys due: float sys.modules[__name__].__annotations__ # {'due': float} ``` On Python 3.14 the dict is materialised on first access rather than being built as the module executes, so reach for it as an attribute of the module object rather than expecting a bare global name to already be bound. ## Why the asymmetry exists The design trade is cost versus usefulness. Class and module annotations are genuinely consulted at runtime — record-building decorators, serialization helpers and introspection tools all walk them — so paying for a dict is worth it. Local annotations have no plausible runtime consumer: locals are not addressable from outside the frame, and the frame is gone when the call returns. Storing them would cost time and memory on every call for information nothing could use, so Python stores nothing. ## What to say in the interview State the three scopes and the two surprises: **a local annotation is not evaluated, so a bogus name in one never fails**, and **a class-body annotation without a value creates no attribute**. Add the `UnboundLocalError` consequence if you want to show you know why the compiler still cares about a statement it does not execute. If you are asked to prove any of it, `__annotations__` on the function, on the class and on the module object is the whole demonstration — and the emptiness of the function's copy is the proof for locals.
- Does a class-body annotation such as `total: float` create a class attribute?No. The name appears in the class's `__annotations__`, but no attribute is created, so `Invoice.total` raises `AttributeError`. Only the assignment in `currency: str = "EUR"` creates an attribute, with the annotation recorded separately. Record-building decorators use exactly that difference to tell a required field from one with a default.
- Why does the compiler care about an annotation on a local name it never stores?Because the annotated name is still a binding target, so the compiler marks it local for the entire function. Reading it before any assignment raises `UnboundLocalError` rather than falling back to a module-level global of the same name — a real behaviour change from a statement that produces no runtime value at all.
- How do you read a module's own annotations at runtime?Through the module object: `sys.modules[__name__].__annotations__`, or the same attribute on any imported module. On Python 3.14 the dict is created when it is first accessed rather than while the module executes, so going through the module attribute is the reliable route.
saying these in an interview costs you the question
- Says local variable annotations land in the function's __annotations__
- Thinks a class-body annotation without a value creates the attribute
- Expects an undefined name in a local annotation to raise
- Confuses parameter annotations with annotations on locals
- Assumes every annotated class variable has a default value