from __future__ import annotations – Opening the Future of Type Hints in Python

Keywords: __future__, annotations, PEP 563, PEP 649, Python 3.7+


1. Why was __future__ needed?



Python has been a dynamic typed language since the early 1990s. As projects grew larger, static type checking and code readability became increasingly important. To address this, Python introduced type hints and the typing module.

However, early type hints were evaluated immediately at runtime, which caused several issues:

Problem Example
Circular references class A: pass
class B(A): pass
Using A directly in a type hint raises NameError
Immediate evaluation from typing import List
def f(x: List[int]) -> List[int]: ...
The List must already be imported at function definition time
String-based delay def f(x: "MyClass") -> "MyClass": ...
To delay evaluation with a string, __future__ is required

To solve these problems, PEP 563 (Python 3.7) introduced from __future__ import annotations.


2. What is from __future__ import annotations?

Definition: A feature introduced in Python 3.7 that stores all type hints as strings, delaying their evaluation.

Core behavior

  • At function/method definition time, type hints are stored as string literals.
  • When the actual type is needed (e.g., via typing.get_type_hints), lazy evaluation occurs.
  • Circular references no longer trigger NameError.

Usage example

# Without __future__
class Node:
    def __init__(self, value: int, next: 'Node' = None):
        self.value = value
        self.next = next

# With __future__
from __future__ import annotations

class Node:
    def __init__(self, value: int, next: Node | None = None):
        self.value = value
        self.next = next

Note: When __future__ is used, all type hints become strings, so functions like typing.get_type_hints automatically evaluate them.


3. Changes after PEP 563



PEP 649 – Redefining lazy evaluation

  • Introduced in Python 3.11.
  • Even with __future__, lazy evaluation occurs only when actually needed.
  • typing.get_type_hints now uses a cache for better performance.

After Python 3.12

  • With full implementation of PEP 649, lazy evaluation becomes even more efficient.
  • from __future__ import annotations is now the default behavior, so explicit declaration is often unnecessary.

4. Practical tips

Situation Recommended approach Reason
Circular references from __future__ import annotations Avoids NameError
Large projects Declare __future__ + use typing.get_type_hints Improves type-checking performance
Python 3.11+ Declaration optional Lazy evaluation is default
String type hints Use typing.TYPE_CHECKING Prevents unnecessary runtime imports

Example: Using typing.TYPE_CHECKING

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .module_a import ClassA

class MyClass:
    def method(self, obj: ClassA) -> None:
        ...

TYPE_CHECKING is True only for static type checkers, so the import is skipped at runtime.


5. Wrap-up

  • from __future__ import annotations enables lazy evaluation of type hints, preventing circular reference errors and runtime issues.
  • Starting with Python 3.11, lazy evaluation is the default, so the declaration is often unnecessary.
  • It remains useful for Python 3.7–3.10 or when explicit lazy evaluation is desired.

Tip: Apply __future__ consistently across your codebase for uniformity. Pairing it with static type checkers like mypy or pyright further elevates code quality.


Further reading - PEP 563: https://www.python.org/dev/peps/pep-0563/ - PEP 649: https://www.python.org/dev/peps/pep-0649/ - mypy documentation: https://mypy.readthedocs.io/en/stable/cheat_sheet.html


Related posts: Check out the following article!