Skip to content

annotationlib Module Complexity

The annotationlib module introspects annotations, which Python 3.14 no longer evaluates when a function, class or module is defined (PEP 649). Its cost is governed by the Format you ask for. On a function, class or module whose annotations the compiler deferred, VALUE evaluates the annotation expressions once, caches the result on the object, and copies it on each call. FORWARDREF uses that cache when every name resolves and otherwise re-runs the __annotate__ function under a substitute namespace on every call. STRING re-runs it on every call, whether or not the names resolve, so once the annotations are cached it is the one format still doing more than a copy.

Complexity Reference

Let n be the number of annotations on the object, e the total length of their expressions' source text, g the number of names in the owning module's globals plus builtins, m the number of names in the owner class's namespace, l the number in a locals argument, and r the total length of the text produced. The bounds count the module's own work: whatever the expressions themselves compute, and any __repr__ they reach, adds its own cost. Terms linear in the number of type parameters, closure cells and __wrapped__ layers an evaluation consults are omitted; they are a handful of names.

Operation Time Space Notes
Format, Format.VALUE, Format.FORWARDREF, Format.STRING O(1) O(1) IntEnum members. Format.VALUE_WITH_FAKE_GLOBALS is internal; get_annotations() and call_annotate_function() reject it with ValueError
get_annotations(obj) with format=Format.VALUE O(n) O(n) Copies the object's cached __annotations__. The first access evaluates the expressions, O(e), and raises NameError for an undefined name; after that they are never evaluated again
get_annotations(obj, format=Format.FORWARDREF) as VALUE when every name resolves, O(e + g) otherwise as VALUE, O(e + g) otherwise Evaluates, caches and copies like VALUE when the expressions evaluate. Otherwise every call attempts the evaluation, then re-runs __annotate__ over a merged copy of the globals, with each unresolved name as a ForwardRef; nothing is cached
get_annotations(obj, format=Format.STRING) O(e) O(e) Runs __annotate__ under a namespace that records every operation, then unparses each result. From 3.14.1 it first runs __annotate__ under the real globals as well, so the calls inside the annotations run on every STRING call, up to the first annotation that raises. Nothing is cached. An object with __annotations__ but no __annotate__, such as a class whose annotations were assigned directly, is converted with annotations_to_string() instead, O(n + r)
get_annotations(obj, eval_str=True) O(n + e + m + l) O(n + e + m + l) Compiles and evaluates each string value with eval() on every call. A class owner's namespace is copied for locals unless both globals and locals are given, and a given locals is copied when the object has type parameters. Only with format=Format.VALUE
call_annotate_function(annotate, format) O(e) for VALUE and STRING, O(e + g) for FORWARDREF O(e), O(e + g) for FORWARDREF The uncached path behind get_annotations(). With a compiler-generated annotate, FORWARDREF merges the builtins and globals into its substitute namespace on every call, whether or not a name is unresolved
call_evaluate_function(evaluate, format) as call_annotate_function() as call_annotate_function() evaluate is a type alias's evaluate_value or a type parameter's evaluate_bound, evaluate_constraints or evaluate_default; returns one value, not a dict
get_annotate_from_class_namespace(namespace) O(1) O(1) Two dict lookups, __annotate__ then __annotate_func__; returns the function or None
annotations_to_string(annotations) O(n + r) O(n + r) New dict with type_repr() of each non-string value; string values pass through unchanged
type_repr(value) O(r) O(r) Qualified name for a class or function, ... for Ellipsis, the source of a template string, repr() otherwise
ForwardRef(arg) O(1) O(1) Stores the string. An expression other than a bare name is compiled on the first evaluate() and the code cached on the reference
ForwardRef.evaluate() O(e + m + l), O(e + m + l + g) for a FORWARDREF retry O(e + m + l), O(e + m + l + g) for a FORWARDREF retry A bare name is a lookup in locals, globals then builtins; any other expression is an eval() of the cached code. With a class owner and no locals, the class namespace is copied first, O(m); a given locals may be copied, and is merged into a retry, O(l). format=Format.STRING returns the source text. format=Format.FORWARDREF returns the reference itself for an unresolved bare name; any other expression that fails is retried over a merged copy of the globals and locals, and the result carries each unresolved name as a ForwardRef, or is the reference itself when the retry fails too

Evaluation Is Deferred, Then Cached

Defining a class does not evaluate its annotations. The first VALUE access does, once; every later call copies the cached dict.

from annotationlib import get_annotations

evaluations = 0

def expensive():
    global evaluations
    evaluations += 1  # runs when the annotation is evaluated, not when defined
    return int

class Config:
    port: expensive()

assert evaluations == 0  # O(1): defining the class evaluated nothing

first = get_annotations(Config)  # O(e): evaluates and caches
assert evaluations == 1

second = get_annotations(Config)  # O(n): copies the cache
assert evaluations == 1
assert first is not second and first["port"] is second["port"]

Choosing a Format

The formats differ in what they return for an undefined name and in what they cost per call.

from annotationlib import Format, ForwardRef, get_annotations

def handler(request: Request, retries: int) -> None:
    pass

# VALUE raises on the first evaluation: Request is not defined
try:
    get_annotations(handler)
except NameError:
    pass

# FORWARDREF re-runs __annotate__ on every call, O(e + g), and wraps
# the unresolved name; a resolved one is the real object
refs = get_annotations(handler, format=Format.FORWARDREF)
assert isinstance(refs["request"], ForwardRef)
assert refs["retries"] is int

# STRING re-runs __annotate__ on every call, O(e), and unparses the source
strings = get_annotations(handler, format=Format.STRING)
assert strings == {"request": "Request", "retries": "int", "return": "None"}

When every name resolves, VALUE and FORWARDREF both copy the cache in O(n), while STRING still re-runs and unparses the expressions on every call. Use STRING for display, and cache its result yourself if it is needed repeatedly.

Evaluating a Forward Reference

ForwardRef.evaluate() resolves a name against the locals given, else the owner's namespace, then the globals and builtins. With a class owner and no locals, the whole class namespace is copied on every call.

from annotationlib import Format, ForwardRef

class Model:
    Id = int

ref = ForwardRef("Id", owner=Model)

assert ref.evaluate() is int  # O(e + m): copies Model's namespace, then looks up
assert ref.evaluate(locals={"Id": str}) is str  # O(e + l): the given locals, copied
assert ref.evaluate(format=Format.STRING) == "Id"  # O(1): the source text

unresolved = ForwardRef("Missing")
assert unresolved.evaluate(format=Format.FORWARDREF) is unresolved

partial = ForwardRef("list[Missing]").evaluate(format=Format.FORWARDREF)  # O(e + g)
assert str(partial) == "list[ForwardRef('Missing')]"

Evaluate Functions

A type alias and a type parameter's bound, constraints and default are also evaluated lazily. call_evaluate_function() applies a Format to one of those expressions and returns a single value.

from annotationlib import Format, call_evaluate_function

type Rows = list[Row]

# STRING: the source text, O(e), without defining Row
assert call_evaluate_function(Rows.evaluate_value, Format.STRING) == "list[Row]"

# FORWARDREF: the real list with Row wrapped, O(e + g)
partial = call_evaluate_function(Rows.evaluate_value, Format.FORWARDREF)
assert str(partial) == "list[ForwardRef('Row')]"

Version Notes

  • Python 3.14+: Module introduced (PEP 749)
  • Python 3.14+: Deferred annotation evaluation default (PEP 649)