Skip to content
← All Python articles

Python explainer

Read Annotations at Runtime in Python 3.14

About 3 min to read. Allow extra time to work through examples.

See when Python 3.14 evaluates annotations, how a class can refer to itself without quotes, and how to read annotations that name something not yet defined.

This article uses Python. You can make it your practice mode to browse related content across the site.

This class describes a tree. Each node points at its parent and its children, so its annotations name the class itself:

class Node:
    parent: Node | None
    children: list[Node]


print(Node.__annotations__)

On Python 3.12 this fails with NameError: name 'Node' is not defined. Python evaluates the annotations while it builds the class, and Node does not exist yet. The usual fix is quotes, parent: "Node | None".

On Python 3.14 the same code runs and prints the real types. Python now evaluates annotations only when code reads them, for example through __annotations__. By then the class exists. This is called deferred evaluation.

The change matters most for tools that read annotations while the program runs: serializers, documentation generators and dependency-injection frameworks. mypy reads your source, so its checks do not change. Run the examples below on Python 3.14 or newer.

Ask for the format you can use

A name in an annotation can still be missing when the annotations are read, for example a type imported only under if TYPE_CHECKING:. The new annotationlib module lets you choose what to get back:

from annotationlib import Format, ForwardRef, get_annotations


def process(widget: Widget, count: int) -> None:
    pass


try:
    get_annotations(process)
except NameError as error:
    print("VALUE:", error)

partial = get_annotations(process, format=Format.FORWARDREF)
assert isinstance(partial["widget"], ForwardRef)
assert partial["count"] is int

print("STRING:", get_annotations(process, format=Format.STRING))
  • Format.VALUE, the default, gives the real objects. It raises NameError for Widget, because nothing defines it.
  • Format.FORWARDREF gives the real object where it can, and a ForwardRef placeholder for each name it cannot find.
  • Format.STRING gives the annotations as text, such as "Widget", for display.

A documentation tool can show the STRING form. A validator that has to know the real type can try FORWARDREF and report the names it could not resolve.

The future import still gives strings

from __future__ import annotations still works on 3.14. With it, Python stores every annotation as a string, and get_annotations returns those strings unless you pass eval_str=True:

from __future__ import annotations

from annotationlib import get_annotations


class Item:
    pass


def consume(item: Item) -> None:
    pass


assert consume.__annotations__ == {"item": "Item", "return": "None"}
assert get_annotations(consume, eval_str=True) == {"item": Item, "return": None}

If your library must also run on Python 3.12, you still need the quotes or the future import. On 3.12, Python evaluates annotations when it defines the function or class.

The first read is cached

Python evaluates the annotations on the first read of __annotations__ and keeps the result. A later change to a name they use does not change them:

Alias = int


def read(value: Alias) -> None:
    pass


Alias = str
assert read.__annotations__["value"] is str
Alias = float
assert read.__annotations__["value"] is str

The first read happened while Alias was str, so the annotation stays str.

Check your understanding On Python 3.14, def handler(req: Request) -> None runs before anything defines Request. When do you get a NameError? Show answer

Only when code reads the annotations' values. Defining the function and calling it both work, because Python has not evaluated Request yet. The error appears when code reads handler.__annotations__, calls get_annotations(handler) or inspect.signature(handler), or when a framework inspects the handler for you.

Going further

Reading annotations can run code, so the STRING and FORWARDREF formats are not a safe way to inspect untrusted objects. inspect.signature also takes an annotation_format argument. The annotationlib reference and the annotations how-to cover the rest, including how to read annotations from a class body before the class exists.

step 26 of 26

Precise Type Hints with Literals and Generics

You've reached the last step

Revisit the track to review its lessons and exercises, or explore another topic.

Explore more tracks