Deferred Annotations
An annotation is the expression attached to a name with a colon —
x: int,def f(a: str) -> bool,class C: count: int. Where that expression gets evaluated, when, and what is stored in the resulting__annotations__dictionary has changed three times over Python’s history. As of CPython 3.14 (the version this note tracks; current patch 3.14.5), annotations are no longer evaluated eagerly: they are compiled into a per-object annotate function (__annotate__) and evaluated lazily, only when something actually asks for them (What’s New in Python 3.14; PEP 649; PEP 749). This is the long-promised fix for two chronic problems — forward references that crashed at definition time, and the CPU cost of evaluating annotations nobody reads at runtime. This note is about where annotations live at runtime and how they are evaluated; the static-checking constructs that go inside an annotation (Optional,TypeVar,Literal, …) are covered in its sibling Type Hints and the typing System.
Mental Model: Three Historical Models
The single most important thing to hold in your head is that “how Python stores annotations” is not one design but a sequence of three, and you must know which one is active to predict what obj.__annotations__ contains.
flowchart TD A["Source: def f(a: Node) -> int: ..."] --> B{"Which model is active?"} B -->|"(a) Legacy eager<br/>(default ≤ 3.13)"| C["Evaluate 'Node' and 'int'<br/>AT def-time.<br/>NameError if Node undefined yet"] B -->|"(b) PEP 563 stringized<br/>(from __future__ import annotations)"| D["Store literal strings<br/>'Node', 'int'.<br/>Never evaluated unless asked"] B -->|"(c) PEP 649/749 deferred<br/>(default in 3.14)"| E["Compile a hidden<br/>__annotate__(format) function.<br/>Evaluate ONLY on access"] C --> F["__annotations__ = {'a': Node, 'return': int}"] D --> G["__annotations__ = {'a': 'Node', 'return': 'int'}"] E --> H["First __annotations__ access<br/>calls __annotate__(1),<br/>caches the dict"]
What this shows: the same source line produces three different runtime outcomes. The insight to extract is that 3.14 changed the default from (a) to (c) — a quiet but significant behavioral shift — while (b) remains opt-in and is on a deprecation path. Under (c) the dictionary you finally get back is identical to what eager evaluation would have produced; the difference is when the work happens and whether undefined forward references explode.
Model (a): Legacy Eager Evaluation
In every Python from the introduction of variable annotations (PEP 526, 3.6) up to and including 3.13, annotations were just ordinary expressions evaluated at the moment the def, class, or annotated assignment executed. Writing def f(a: Node) ran the name Node immediately; if Node was defined later in the file (a forward reference) or in a module that would create a circular import, you got a NameError at definition time. The workaround was to quote the offending annotation as a string literal — def f(a: "Node") — which the runtime stored verbatim and left for a type checker (or typing.get_type_hints) to resolve later. Eager evaluation also meant that every annotation expression in a large module ran at import time, adding measurable startup cost for code that may never introspect a single hint. These two pain points — forward references and import cost — are the motivation for everything that follows (PEP 649 Abstract).
Model (b): PEP 563 Stringized Annotations
PEP 563 (Python 3.7) offered an escape hatch: from __future__ import annotations at the top of a module. With it active, the compiler never evaluates any annotation — it stores each one as the string of its source text. So under model (b), def f(a: Node) -> int yields __annotations__ == {'a': 'Node', 'return': 'int'} with no evaluation, no NameError, and no import cost. Forward references “just work” because nothing is resolved until a consumer explicitly calls eval() on the string (typically via typing.get_type_hints, which evaluates the strings in the object’s namespace).
PEP 563 was originally slated to become the default in 3.10, then 3.11, but the schedule was repeatedly postponed because stringization breaks runtime libraries that rely on getting real objects out of __annotations__ — Pydantic, dataclasses, FastAPI, and similar frameworks that introspect types at runtime. The PEP now states plainly that its features “never became the default behaviour, and have been replaced with deferred evaluation of annotations, as proposed by PEP 649 and PEP 749” (PEP 563). PEP 563 still exists and still works in 3.14, but it is now a legacy mode (details below).
Model (c): PEP 649/749 Deferred Evaluation — the 3.14 Default
This is the model 3.14 ships. The What’s New document states it directly:
“The annotations on functions, classes, and modules are no longer evaluated eagerly. Instead, annotations are stored in special-purpose annotate functions and evaluated only when necessary (except if
from __future__ import annotationsis used).” (What’s New in Python 3.14)
The mechanism, specified in PEP 649 and implemented per PEP 749, works like this. When the compiler sees an object that carries annotations, instead of emitting bytecode that evaluates those annotation expressions at definition time, it compiles a separate annotate function and stores it on the object as __annotate__. That function takes one positional-only integer argument — a format code — and returns the annotations dictionary computed in the requested format. The object’s __annotations__ attribute is redefined as a data descriptor: when you read obj.__annotations__ and the cached storage is empty, the descriptor getter calls obj.__annotate__(1) — format code 1 is VALUE — evaluates the real annotation values, caches the resulting dict, and returns it. PEP 649 puts the trigger precisely:
“When
o.__annotations__is evaluated, and the internal storage foro.__annotations__is unset, ando.__annotate__is set to a callable, the getter … callso.__annotate__(1).” (PEP 649)
Because the annotation expressions are now wrapped in a function body that runs on demand, forward references stop being a problem: by the time anyone reads __annotations__, the names they reference are usually defined. And because the function only runs when called, a module full of annotations costs nothing at import beyond the cheap construction of the __annotate__ closures. As What’s New summarizes: “It is no longer necessary to enclose annotations in strings if they contain forward references.”
How __annotate__ and __annotations__ interact
sequenceDiagram participant U as User code participant D as __annotations__ descriptor participant Cache as cached dict participant Ann as __annotate__(format) U->>D: read obj.__annotations__ D->>Cache: cached? alt cache empty and __annotate__ set D->>Ann: call __annotate__(1) (VALUE) Ann-->>D: {'a': Node, 'return': int} D->>Cache: store result end Cache-->>U: return dict Note over U,Ann: Second access hits the cache;<br/>__annotate__ is NOT called again
What this shows: the lazy-evaluate-and-cache path. The insight: __annotations__ and __annotate__ are two faces of the same data. __annotate__ is the recipe (a function that can produce annotations in several formats); __annotations__ is the cached VALUE-format result. Setting __annotations__ explicitly (e.g. obj.__annotations__ = {...}) clears/overrides the lazy path. Each of functions, classes, and modules carries its own __annotate__; the Function Objects and Code Objects note describes the function object that holds it, and Module Objects and Namespaces covers the module-level case where __annotate__ lives in the module’s namespace.
The annotationlib Module (New in 3.14)
The interesting consequence of the annotate-function design is that a single object can produce its annotations in more than one format — and that is what the new annotationlib module exposes (annotationlib docs). The formats are an IntEnum whose exact values, taken from the released v3.14.5 source, are:
class Format(enum.IntEnum):
VALUE = 1 # evaluated real values (default)
VALUE_WITH_FAKE_GLOBALS = 2 # internal use only
FORWARDREF = 3 # real values where defined, ForwardRef proxies where not
STRING = 4 # the source text of each annotationLine by line: VALUE (1) is the classic behavior — evaluate each annotation expression and return the resulting objects; this is what plain __annotations__ access uses. VALUE_WITH_FAKE_GLOBALS (2) is an internal sentinel the compiler-generated annotate functions understand; it signals “you are being run in a synthetic namespace,” and user code should never request it. FORWARDREF (3) is the clever one: it evaluates what it can, but any name that is not yet defined is returned as a ForwardRef proxy object instead of raising NameError — so a tool can introspect annotations even mid-import, with unresolved names left as resolvable placeholders. STRING (4) returns the source text of each annotation (this format was called SOURCE in PEP 649 and was renamed to STRING in PEP 749 — PEP 749, “Renaming SOURCE to STRING”).
The public entry point is get_annotations(), whose signature is:
annotationlib.get_annotations(obj, *, globals=None, locals=None,
eval_str=False, format=Format.VALUE)obj is any function, class, module, or object carrying __annotate__/__annotations__. format selects one of the enum members above. eval_str, when true, runs eval() over any annotation that is already a plain string (the PEP 563 case). globals/locals are the namespaces used for that evaluation; they default sensibly — a module’s __dict__, a class’s module globals plus class namespace, or a callable’s __globals__. The docs state outright that “calling get_annotations() is best practice for accessing the annotations dict of any object” (annotationlib docs).
A worked example from the docs makes the three formats concrete:
>>> from annotationlib import get_annotations, Format
>>> def func(arg: Undefined): # 'Undefined' is never defined anywhere
... pass
>>> get_annotations(func, format=Format.VALUE) # tries to evaluate -> boom
NameError: name 'Undefined' is not defined
>>> get_annotations(func, format=Format.FORWARDREF) # graceful
{'arg': ForwardRef('Undefined', owner=<function func ...>)}
>>> get_annotations(func, format=Format.STRING) # never evaluates
{'arg': 'Undefined'}The VALUE call raises because Undefined cannot be resolved. The FORWARDREF call succeeds and hands back a ForwardRef whose __forward_arg__ is the string "Undefined" and which can be .evaluate()d later once the name exists. The STRING call sidesteps evaluation entirely and returns the source text — the same thing PEP 563 stringization would have stored. The module also offers ForwardRef.evaluate(...), plus low-level helpers call_annotate_function(annotate, format, *, owner=None) (run an annotate function and return its dict) and call_evaluate_function(...) (run the single-value evaluate functions that back TypeAliasType.__value__, TypeVar.__bound__, and friends — the lazy machinery from Type Hints and the typing System).
PEP 563 Under 3.14, and Its Deprecation Path
from __future__ import annotations still works in 3.14, and it composes with the new model in a specific way: PEP 749 specifies that “if the future import is active, the __annotate__ function of objects with annotations will return the annotations as strings when called with the VALUE format” (PEP 749). In other words, the annotate function still exists, but it has been compiled to emit STRING-format text even for VALUE requests — so __annotations__ under the future import gives you the same string dict it always did.
PEP 563 is, however, now on a removal track. PEP 749 lays out the schedule:
After Python 3.13 reaches end of life, “Compiling any code that uses the future import will emit a
DeprecationWarning.” Two or more releases later, “Code that continues to use the future import will raise aSyntaxError.” (PEP 749)
Uncertain
Verify (as of 2026-06-01): the concrete releases in which
from __future__ import annotationsstarts emitting aDeprecationWarningand later becomes aSyntaxError. Reason: no fixed version exists yet to cite. PEP 749 (re-read at this date) deliberately phrases the schedule relative to events — deprecation begins “sometime after the last release that did not support PEP 649 semantics (expected to be 3.13) reaches its end-of-life,” and removal follows “after at least two releases” — and What’s New in 3.14 names no version at all for this. Such schedules also slip historically (PEP 563 was deferred from 3.10 to 3.11 and then indefinitely). To resolve: watch the What’s New / deprecation index of the post-3.13-EOL releases for the first one that pins a concrete deprecation version, then update this with the named release. uncertain
Failure Modes and Common Misunderstandings
The most common surprise is code that worked under 3.13’s eager model and now behaves differently. Two cases dominate. First, code that accessed __annotations__ and got real objects but had quietly relied on a side effect of eager evaluation (e.g. an annotation expression with an import side effect) no longer triggers that side effect until the annotation is read — which may be never. Second, libraries that read obj.__annotations__ directly and expected strings (because the project used from __future__ import annotations) versus those that expected objects (because it did not) can now silently get the other kind unless they migrate to annotationlib.get_annotations(obj, format=...), which is format-explicit. The fix the ecosystem is converging on is: stop reading the raw __annotations__ / __annotate__ attributes and call annotationlib.get_annotations with the format you actually want, using FORWARDREF when you must introspect possibly-unresolved annotations (mid-import, circular dependencies).
A subtler trap: __annotate__ is None for objects that have no annotations, and reading __annotations__ on such an object returns an empty dict rather than calling a (nonexistent) annotate function. Tools must check for the None case rather than assuming every object has a callable __annotate__.
Alternatives: get_type_hints, inspect.get_annotations, annotationlib
There are now three runtime ways to fetch annotations, and they are related but distinct. typing.get_type_hints(obj) is the oldest type-checking-oriented accessor: it resolves string annotations, strips Annotated[...] metadata by default, and substitutes None for type(None) — it answers “what types does the checker see,” not “what is the literal annotation.” inspect.get_annotations() (added in 3.10) was the stopgap that returned the raw annotations dict with optional eval_str. As of 3.14, annotationlib.get_annotations() is the format-aware successor and the recommended general-purpose tool, because only it understands the FORWARDREF/STRING formats the deferred model enables. Use typing.get_type_hints when you want resolved checker-style types; use annotationlib.get_annotations when you want format control or are introspecting un-runnable annotations. The static-typing side of get_type_hints is discussed in Type Hints and the typing System.
Production Notes
The frameworks that drove this redesign — Pydantic, dataclasses, attrs, FastAPI, SQLAlchemy’s typed mappings — all introspect annotations at runtime, which is precisely why PEP 563’s “everything is a string” model was rejected as the default: it would have forced every such library to ship an eval()-based resolver and reckon with the namespace in which to evaluate. PEP 649/749’s deferred model gives these libraries the best of both worlds: forward references and zero import cost (the PEP 563 wins) while still being able to obtain real objects on demand via Format.VALUE, and graceful ForwardRef proxies via Format.FORWARDREF when a name genuinely is not resolvable yet. For library authors, the migration guidance in What’s New is concrete: replace direct __annotations__ reads with annotationlib.get_annotations, and prefer the FORWARDREF format in code that runs during import.
See Also
- Type Hints and the typing System — sibling; the constructs (
Optional,TypeVar,Literal,TypedDict, PEP 695 syntax) that go inside an annotation, plus the static-vs-runtime divide. - Function Objects and Code Objects — where
__annotate__and__annotations__live on a function object. - Module Objects and Namespaces — module-level annotations and the module
__annotate__. - Protocols and Structural Typing —
Protocol, anothertypingconstruct. - MOC: Python Internals MOC §13 The Type System and Data-Model Protocols.