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
# 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.

python
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

Sources