PEP 649/749: Lazy Annotations
Python 3.14 fundamentally changes how type annotations are handled. With PEP 649 (complemented by PEP 749), annotations are no longer evaluated at class or function definition time: they are stored as lazy descriptors and only resolved when __annotations__ is accessed.
Difference from PEP 563 (from __future__)
PEP 563 (from __future__ import annotations) turned annotations into strings. PEP 649 keeps them as on-demand evaluable expressions, which preserves compatibility with runtime validation libraries like Pydantic or attrs.
# Python 3.14: annotations are lazily evaluated
# No more need for from __future__ import annotations
class Node:
"""Forward reference: works natively."""
value: int
child: Node | None = None # no NameError!
# The 'Node' annotation is not evaluated at definition time.
# It is only resolved when accessed:
print(Node.__annotations__) # {'value': <class 'int'>, 'child': ...}
# Use get_annotations() for safe access:
import annotationlib
ann = annotationlib.get_annotations(Node, format=annotationlib.Format.FORWARDREF)
print(ann) # resolves forward references correctly
Impact on Runtime Type Checking
Libraries that inspect annotations at runtime (Pydantic, dataclasses, FastAPI) benefit directly from this change: they receive real Python objects instead of strings to parse, while still supporting forward references.
from dataclasses import dataclass
@dataclass
class Tree:
value: int
left: Tree | None = None
right: Tree | None = None
def depth(self) -> int:
l = self.left.depth() if self.left else 0
r = self.right.depth() if self.right else 0
return 1 + max(l, r)
tree = Tree(
1,
left=Tree(2, left=Tree(4)),
right=Tree(3),
)
print(tree.depth()) # 3
# Annotations are real types, not strings:
import annotationlib
ann = annotationlib.get_annotations(Tree)
print(ann['left']) # Tree | None
