Python 3.14 Deferred Annotations

Python 3.14 evaluates annotations only when code asks for them. A function, class, or module can therefore refer to a name that is defined later, without quoting that name merely to avoid an error during definition. This behavior is called deferred evaluation of annotations.

Deferred annotations reduce import-time work and make forward references easier to write. They mainly affect libraries and tools that inspect annotations at runtime; static type checkers still analyze the type information without running the program.

Version requirement: Deferred evaluation is the default in Python 3.14 and newer. Earlier versions use eager evaluation unless another annotations model is enabled.

How Annotation Evaluation Changed

Model When it applies Stored behavior
Eager evaluation Default through Python 3.13 Expressions run when the definition executes
Stringized annotations With the future annotations import Annotations are stored as strings
Deferred evaluation Default from Python 3.14 Expressions are evaluated when annotations are requested

Python compiles annotation expressions into a separate annotation function. Access through the normal annotations interface can call that function later and cache the resulting dictionary. Code that never inspects the annotations avoids that evaluation work.

Use a Forward Reference without Quotes

In Python 3.13, the first definition below would raise NameError because Order and Receipt do not exist yet. Python 3.14 keeps the expressions for later, so the names can resolve after both classes have been defined.

Example:

from annotationlib import get_annotations

# These classes are intentionally defined after the function.
def deliver(order: Order) -> Receipt:
    return Receipt(order.number)

class Order:
    def __init__(self, number: str):
        self.number = number

class Receipt:
    def __init__(self, order_number: str):
        self.order_number = order_number

# Request the evaluated annotation values after the names exist.
annotations = get_annotations(deliver)
print(annotations["order"].__name__)
print(annotations["return"].__name__)

Output:

Order
Receipt

Inspect Annotations with annotationlib

Python 3.14 adds the annotationlib module as the main interface for runtime annotation inspection. Its get_annotations() function returns a new dictionary and can request annotations in three useful public formats.

Format Result Best use
VALUE Evaluated Python objects Frameworks that need actual types or values
FORWARDREF Values plus ForwardRef objects for unresolved names Tools that tolerate incomplete definitions
STRING Readable strings close to the source expression Documentation and display tools

Handle an Unresolved Name

The default VALUE format can raise an exception when an annotation still contains an undefined name. Use FORWARDREF when your tool must continue and represent that missing name, or STRING when you only need readable text.

Example:

from annotationlib import Format, get_annotations

# FutureJob is not defined in this module.
def handoff(job: FutureJob) -> None:
    pass

# Keep unresolved names as ForwardRef objects.
forward = get_annotations(handoff, format=Format.FORWARDREF)
text = get_annotations(handoff, format=Format.STRING)

print(type(forward["job"]).__name__)
print(text["job"])

Output:

ForwardRef
FutureJob

Choose the Right Runtime API

Use annotationlib.get_annotations() for Python 3.14 code. It handles functions, classes, modules, wrapped functions, and the new formats more consistently than reading __annotations__ yourself. For software that supports Python 3.10 through 3.13, inspect.get_annotations() remains the compatible standard-library choice.

Example:

import sys

# Select the supported inspection helper for this Python version.
if sys.version_info >= (3, 14):
    from annotationlib import get_annotations
else:
    from inspect import get_annotations

print(get_annotations)

Understand the Future Import

The statement from __future__ import annotations still selects stringized annotations in Python 3.14. It does not enable the new deferred-value model. Existing projects can keep it during migration, but new runtime-inspection code should account for strings and use the documented helpers instead of assuming a specific dictionary shape.

Example:

from __future__ import annotations
from annotationlib import get_annotations

# The future import keeps annotations in stringized form.
def total(values: list[int]) -> int:
    return sum(values)

print(get_annotations(total))

Output:

{'values': 'list[int]', 'return': 'int'}

Avoid Side Effects and Unsafe Inspection

Annotations may contain arbitrary expressions. Evaluating them can run code, raise an exception, perform slow work, or depend on state that has changed since definition time. Keep your own annotations simple and side-effect free.

  • Do not inspect annotations from untrusted code.
  • Do not place function calls with side effects inside annotations.
  • Expect unresolved names when modules load in cycles or plugins are optional.
  • Use the least demanding format that solves your task.
  • Do not modify the returned or stored annotations as application state.

Security note: Even string-oriented annotation inspection can execute code in unusual expressions. Treat annotation introspection with the same care as importing and executing the module.

Conclusion

Python 3.14 deferred annotations make forward references natural and postpone annotation work until it is needed. Use annotationlib.get_annotations() for runtime access, select VALUE, FORWARDREF, or STRING deliberately, and keep annotations free of side effects. Libraries that support older Python versions should retain a clear compatibility path.



Found This Page Useful? Share It!
Get the Latest Tutorials and Updates
Join us on Telegram