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.