Python annotationlib, added in Python 3.14, provides low-level tools for retrieving and evaluating annotations on functions, classes, modules, and related objects. It supports the deferred-annotation model, unresolved forward references, metaclass workflows, and libraries that need runtime type information without requiring every name to exist when the annotation is created.
The module is more than a safe dictionary reader. Many operations may execute expressions contained in annotations. Frameworks, plugin systems, documentation generators, and validation libraries must therefore treat annotation introspection as code execution.
Why annotation semantics changed
Through Python 3.13, annotations were normally evaluated while a function or class body executed. That caused failures for names declared later and increased import-time work. from __future__ import annotations stored strings, but runtime libraries then had to evaluate and reconstruct them.
Python 3.14 uses deferred evaluation by default. Annotation expressions are evaluated when requested, and annotationlib exposes a consistent interface for retrieving them in several formats.
Use get_annotations
get_annotations() is the primary API. It accepts a callable, class, module, or another object with supported annotation attributes and returns a new dictionary each time.
from annotationlib import get_annotations
def calculate(value: int, scale: float) -> float:
return value * scale
print(get_annotations(calculate))Prefer this function over reading __annotations__ directly because it handles lazy annotate functions, wrappers, class behavior, and older stringified annotations more reliably.
The three main formats
The Format enum controls the result:
VALUEevaluates expressions and returns runtime objects.FORWARDREFreturns real values when possible and proxy objects for unresolved names.STRINGreturns approximate source-like text.
from annotationlib import Format, get_annotations
def process(item: ModelDefinedLater) -> list[str]:
...
refs = get_annotations(process, format=Format.FORWARDREF)
texts = get_annotations(process, format=Format.STRING)Select the format according to the consumer rather than evaluating everything automatically.
VALUE format
When every referenced name is available, VALUE returns classes, aliases, generic objects, and other evaluated values.
class User:
pass
def save(user: User) -> None:
pass
annotations = get_annotations(save, format=Format.VALUE)
assert annotations['user'] is UserEvaluation may call arbitrary code embedded in an annotation expression. Do not use this format for annotations constructed from untrusted input.
FORWARDREF format
If a name cannot be resolved, the module can return a ForwardRef instead of raising NameError.
def load(value: FutureModel) -> FutureModel:
pass
annotations = get_annotations(
load,
format=Format.FORWARDREF,
)
reference = annotations['value']A proxy returned by get_annotations() may retain information about its originating scope, which can help later evaluation.
Evaluate a ForwardRef
ForwardRef.evaluate() attempts to resolve the expression. Supply an owner or explicit namespaces when needed.
class FutureModel:
pass
resolved = reference.evaluate(
globals=globals(),
locals=locals(),
)
assert resolved is FutureModelUsing Format.FORWARDREF during evaluation can preserve still-unresolved pieces as proxies. Format.STRING returns the stored expression text.
STRING format
STRING aims to return readable annotation text:
display = get_annotations(
save,
format=Format.STRING,
)
print(display)The result is not guaranteed to reproduce exact source. Comments, whitespace, parentheses, numeric representation, and compiler simplifications may differ. Some expression forms are unsupported or approximate.
STRING is not a sandbox
Requesting text can still execute operations needed by the stringifier. Carefully crafted annotation expressions may call functions or traverse object attributes even without ordinary globals.
Never accept user text, place it in an annotation dictionary, and pass it to these APIs. Documentation for unknown code should be generated in an isolated process, preferably by parsing source with AST.
eval_str for older stringified annotations
Objects created with from __future__ import annotations may store strings. Setting eval_str=True asks get_annotations() to evaluate those strings.
annotations = get_annotations(
object_to_inspect,
eval_str=True,
globals=global_namespace,
locals=local_namespace,
)This option is valid only with Format.VALUE and carries the normal risks of eval().
Convert values to strings
annotations_to_string() converts a runtime annotation mapping into display strings.
from annotationlib import annotations_to_string
result = annotations_to_string({
'item': list[int],
'return': type(None),
})It is useful for custom annotation functions that do not have access to original source.
Use type_repr
type_repr() produces a human-oriented representation for types and common annotation objects.
from annotationlib import type_repr
print(type_repr(dict[str, int]))Do not use the resulting string as a permanent identifier or a serialization format because representations may evolve.
Working with __annotate__
Under deferred semantics, the compiler may create a __annotate__ callable. call_annotate_function() invokes that callable with a requested format.
from annotationlib import call_annotate_function, Format
values = call_annotate_function(
MyClass.__annotate__,
Format.FORWARDREF,
owner=MyClass,
)Most libraries should stay with get_annotations(). Direct annotate-function APIs are mainly for frameworks, metaclasses, and typing implementations.
Inspect annotations during class construction
A metaclass receives a namespace before the final class exists. get_annotate_from_class_namespace() locates the annotate function inside that dictionary.
import annotationlib
class ModelMeta(type):
def __new__(mcls, name, bases, namespace):
annotate = annotationlib.get_annotate_from_class_namespace(
namespace
)
if annotate:
fields = annotationlib.call_annotate_function(
annotate,
annotationlib.Format.FORWARDREF,
)
validate_fields(fields)
return super().__new__(mcls, name, bases, namespace)Forward references are usually preferable at this stage because the class and related names may not exist yet.
Evaluate type-alias values
call_evaluate_function() works with lazy evaluate functions on type aliases and type parameters. It can return a value, forward-reference proxy, or string.
value = annotationlib.call_evaluate_function(
Alias.evaluate_value,
annotationlib.Format.FORWARDREF,
owner=Alias,
)This is an advanced API intended for typing-aware frameworks.
Combine with inspect
The Python inspect guide covers signatures and live objects. A framework can combine inspect.signature() with get_annotations() to preserve parameter order while choosing a safe annotation format.
Understand scope resolution
The function chooses context-sensitive namespaces. Modules use their dictionaries, classes use their module globals and class namespace, and callables generally use their function globals after known wrappers are unwrapped.
For the compiler’s classification of local, global, nonlocal, free, and type-parameter names, read the Python symtable guide.
Wrappers and partial objects
get_annotations() understands functions wrapped with functools.update_wrapper() and functools.partial. Custom decorators should maintain __wrapped__ correctly so introspection reaches the intended function.
Class inheritance behavior
Class annotations are not inherited automatically by this API, and metaclass annotations are ignored. If a class has no annotations of its own, the function returns an empty dictionary. Frameworks that merge schemas across an MRO must define explicit precedence rules.
The Python types guide describes related runtime type utilities.
Python-version compatibility
annotationlib exists in Python 3.14 and later. Libraries supporting earlier versions need a guarded import or an appropriate backport from typing_extensions. Test behavior on every supported interpreter because annotation semantics differ substantially.
Security implications
- Annotation expressions may execute arbitrary Python code.
ForwardRef.evaluate()may useeval().STRINGdoes not guarantee non-execution.- Do not process annotation values assembled from external input.
- Do not expose sensitive globals to evaluation.
- Apply time and memory limits in documentation workers.
- Inspect untrusted plugins in a separate process.
Testing strategy
Test names defined later, aliases, generic classes, native type parameters, wrapped functions, future-string annotations, unresolved names, and annotations with deliberate side effects. Exercise all three formats independently.
Avoid exact string assertions for whitespace and parentheses. Instead, test semantic content because compiler representations may change.
Recommended practices
- Use
get_annotations()as the main entry point. - Select the format explicitly.
- Prefer
FORWARDREFduring class construction. - Use
STRINGfor display, not trust boundaries. - Expect evaluation errors from the annotation itself.
- Keep compatibility branches for older Python versions.
- Do not mutate annotation dictionaries with user input.
- Document the framework’s evaluation policy.
Conclusion
Python annotationlib provides the standard low-level interface for introspecting deferred annotations in Python 3.14. It can return evaluated objects, unresolved proxies, or readable strings and offers advanced helpers for metaclasses and typing constructs.
Its flexibility requires caution because introspection may execute arbitrary code. Read the official annotationlib documentation, PEP 649, and PEP 749.







