Using future annotations for Forward References
TL;DR — from __future__ import annotations (PEP 563) stores every annotation in a module as an unevaluated string instead of a live object. That lets forward references and new-syntax unions like int | None work without quotes on older runtimes, because the annotation is never executed at definition time. The catch: any tool that reads annotations at runtime must now call typing.get_type_hints() to resolve those strings, and some libraries — notably Pydantic v1 — break on the deferred form.
A forward reference is a name used in an annotation before it exists — a method returning its own class, or two classes that reference each other. Without deferred evaluation you quote the name ("Node") so Python does not try to look it up too early. The future import automates that: it quotes everything for you. This page shows the before/after, a self-referencing class, and the runtime caveat that decides whether you can adopt it.
get_type_hints() to turn it back into a type.Before and after
Without the future import, a new-syntax union or an as-yet-undefined name in an annotation is evaluated when the function is defined. On Python 3.9 int | None raises, and a forward reference must be quoted:
# Python 3.9, WITHOUT the future import
from typing import Optional
def find(node_id: int) -> "Node | None": # must quote: Node not yet defined,
... # and | unsupported at runtime on 3.9
class Node:
...
Add the import at the very top of the module and the quotes and the runtime restriction both disappear — every annotation is now a string the interpreter never runs:
# Python 3.9+, WITH the future import
from __future__ import annotations
def find(node_id: int) -> Node | None: # no quotes; stored as the string "Node | None"
...
class Node:
...
from __future__ import annotations must be the first statement in the file (after the module docstring). It applies module-wide — you cannot enable it per function. It relates directly to the Union and Optional modernization story, since it is what lets pipe unions compile on pre-3.10 runtimes.
A self-referencing class
The cleanest payoff is a recursive data structure. A tree node whose children are the same type would otherwise need every self-reference quoted:
# Python 3.10+, checked with mypy 1.10 / pyright 1.1.370
from __future__ import annotations
from dataclasses import dataclass, field
@dataclass
class TreeNode:
value: int
parent: TreeNode | None = None # self-reference, unquoted
children: list[TreeNode] = field(default_factory=list)
def add(self, value: int) -> TreeNode: # returns its own type
child = TreeNode(value, parent=self)
self.children.append(child)
return child
Every mention of TreeNode inside its own body is a forward reference that the future import turns into a harmless string. mypy and pyright resolve those strings statically without any runtime cost, so the annotations are fully checked.
The runtime caveat
Deferring annotations to strings is free for static analysis but not for code that reads annotations while the program runs. Dataclasses store the raw strings in __annotations__; anything that needs the real types must resolve them:
# Python 3.10+, resolving deferred annotations at runtime
from __future__ import annotations
from typing import get_type_hints
class Handler:
retry: RetryPolicy
class RetryPolicy: ...
hints = get_type_hints(Handler) # {"retry": <class '...RetryPolicy'>} — resolved
raw = Handler.__annotations__ # {"retry": "RetryPolicy"} — just a string
get_type_hints() re-evaluates each string in the class’s module globals, so a name that is not importable in that scope raises NameError. This is why some frameworks stumble: Pydantic v1 reads annotations eagerly and does not always resolve deferred strings, so models can fail to build or silently mistype fields; Pydantic v2 handles PEP 563 correctly. Any library relying on __annotations__ directly — older serializers, some ORM layers — needs the same get_type_hints() treatment or it sees strings where it expects types.
Analyzer behavior
Static checkers are unaffected by the deferral in a good way: mypy and pyright both resolve the string annotations against module scope exactly as if they were live, so a wrong self-reference or an unresolvable name still surfaces — mypy [name-defined], pyright reportUndefinedVariable. Neither checker requires the future import to validate pipe unions; it only matters for whether the annotation runs on your target interpreter. ruff can even add the import for you via the from-future-import-annotations rule family and rewrite quoted forward refs. Pair it with your mypy configuration so undefined forward references stay an error.
from __future__ import annotations, checkers see fully-resolved types but the interpreter sees only strings in __annotations__. Code that introspects annotations at runtime must call typing.get_type_hints(), which re-evaluates each string in the defining module's scope and raises NameError if a referenced name is not importable there. Pydantic v1 and other eager annotation readers can break; verify runtime consumers before enabling it globally.
Common mistakes
- Placing the import below other code.
from __future__ import annotationsmust be the first statement (after any docstring), or Python raisesSyntaxError: from __future__ imports must occur at the beginning of the file. - Assuming runtime tools still see types. After the import,
Handler.__annotations__holds strings; frameworks that read it directly get strings, not classes. Resolve withget_type_hints(). - Forgetting a referenced name is out of scope.
get_type_hints()fails withNameErrorif a forward-referenced type is not importable in the module globals — a locally-defined type inside a function cannot be resolved. - Enabling it under Pydantic v1. Deferred annotations can leave v1 models mistyped or failing to build; upgrade to v2 or keep those modules on eager, quoted annotations.
FAQ
Does the future import change what mypy or pyright report?
No. Both resolve the string annotations against module scope and check them exactly as if they were evaluated. Undefined forward references still error (mypy [name-defined]); the import only affects whether the annotation executes on your runtime.
Will PEP 563 become the default so I can drop the import? It was proposed as a default and then deferred indefinitely because of the runtime-introspection breakage described above. For now you must opt in per module with the explicit import; do not assume a future Python turns it on automatically.