Python annotationlib: Read Annotations

Published on: August 14, 2026
Reading time: 5 minutes
Code with type annotations representing introspection with Python annotationlib

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:

  • VALUE evaluates expressions and returns runtime objects.
  • FORWARDREF returns real values when possible and proxy objects for unresolved names.
  • STRING returns 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 User

Evaluation 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 FutureModel

Using 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 use eval().
  • STRING does 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.

  • Use get_annotations() as the main entry point.
  • Select the format explicitly.
  • Prefer FORWARDREF during class construction.
  • Use STRING for 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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Organized archive binders representing modules imported directly from ZIP files with Python zipimport
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python zipimport: Import from ZIP Files

    Learn Python zipimport to load modules and packages from ZIP archives, work with importers, and protect plugin systems.

    Ler mais

    Tempo de leitura: 5 minutos
    14/08/2026
    Directory diagram representing site-packages paths and Python site module configuration
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python site: Understand Package Paths

    Learn the Python site module to understand site-packages, user site, .pth files, sitecustomize, usercustomize, virtual environments, and startup flags.

    Ler mais

    Tempo de leitura: 6 minutos
    14/08/2026
    Installer icon representing offline pip bootstrap with Python ensurepip
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ensurepip: Restore pip Offline

    Learn Python ensurepip to install or restore pip offline, choose the environment, script names, upgrade behavior, and avoid system conflicts.

    Ler mais

    Tempo de leitura: 5 minutos
    14/08/2026
    Software package representing package metadata inspected with Python importlib.metadata
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python importlib.metadata: Package Data

    Learn Python importlib.metadata to inspect installed versions, dependencies, files, metadata, entry points, and import-to-distribution mappings.

    Ler mais

    Tempo de leitura: 5 minutos
    14/08/2026
    Executing code representing modules and paths run with Python runpy
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python runpy: Execute Modules and Paths

    Learn Python runpy to execute modules, scripts, directories, and ZIP files, control namespaces, and avoid security and thread-safety problems.

    Ler mais

    Tempo de leitura: 5 minutos
    13/08/2026
    Software package representing module discovery with Python pkgutil
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python pkgutil: Discover Packages

    Learn Python pkgutil to discover modules, walk packages, resolve objects, extend package paths, and access resources safely.

    Ler mais

    Tempo de leitura: 5 minutos
    13/08/2026