Skip to content

What is PEP 649?

PEP 649: Deferred Evaluation of Annotations Using Descriptors changes when Python evaluates type annotations. From Python 3.14, annotations are no longer computed at function or class definition time; they’re computed lazily, on first access. Its companion PEP 749: Implementing PEP 649 adds the annotationlib module for inspecting annotations under the new model.

Together they resolve a tension that goes back to PEP 484 itself: type annotations that reference names defined later in the file used to crash at import time, and the workaround (from __future__ import annotations) broke libraries that read annotations at runtime.

What changed in Python 3.14?

Forward references now work without quotes or future imports:

class Node:
    next: Node | None = None  # 3.14: fine; older versions: NameError at class creation

Under the old eager model, Python evaluated Node | None while still defining Node, so the name didn’t exist yet. Under PEP 649, the interpreter compiles the annotation into a hidden __annotate__ function and only calls it when something first asks for Node.__annotations__. A class whose annotations mention completely undefined names now defines without error; the evaluation (and any NameError) happens at inspection time instead of import time.

Code that never inspects annotations, which is most code, pays nothing for them.

What does annotationlib provide?

Runtime tools ask for annotations in one of three formats (a fourth, VALUE_WITH_FAKE_GLOBALS, is reserved for tool internals):

  • VALUE evaluates the annotations and returns real objects. This raises NameError if a referenced name doesn’t exist yet.
  • FORWARDREF returns real objects where possible and ForwardRef placeholders for names that don’t resolve yet.
  • STRING returns the annotation source text without evaluating anything.
import annotationlib

class C:
    x: Undefined  # never defined anywhere

annotationlib.get_annotations(C, format=annotationlib.Format.FORWARDREF)
# {'x': ForwardRef('Undefined', is_class=True, owner=<class 'C'>)}

annotationlib.get_annotations(C, format=annotationlib.Format.STRING)
# {'x': 'Undefined'}

inspect.signature() gained a matching annotation_format parameter. typing.get_type_hints() keeps working as before; annotationlib.get_annotations() is the more capable replacement.

The FORWARDREF format is the headline for runtime tools. Validators like Pydantic and the stdlib’s own dataclasses read annotations while building classes, often before every referenced name exists; FORWARDREF lets them collect what resolves and revisit the placeholders later instead of crashing.

Do you still need from __future__ import annotations?

For code that runs only on 3.14+: no. The future import was the standard fix for forward references and for the import-time cost of evaluating annotations, and PEP 649 addresses both by default.

For code supporting older versions: keep it until your minimum is 3.14. The future import still works on 3.14 (it switches that module back to PEP 563 string annotations), and CPython has committed to keeping it at least until Python 3.13 reaches end of life in October 2029 (python/cpython#127639).

Static type checkers like mypy and ty never evaluate annotations, so they behave the same under all three regimes. The differences only bite tools that inspect annotations at runtime.

Learn More

Last updated on