Skip to main content

Overview

Type annotations can sometimes cause runtime errors in Python. This guide shows how to resolve these issues using:
  • String literal types and type comments
  • typing.TYPE_CHECKING
  • from __future__ import annotations (PEP 563)

String literal types and type comments

String literals prevent runtime evaluation of annotations:
def f(a: list['A']) -> None: ...  # OK, prevents NameError

class A: pass
Any type can be a string literal, and you can mix string and non-string types freely.

Type comments (deprecated)

Type comments are the older syntax required before Python 3.6:
a = 1  # type: int

def f(x):  # type: (int) -> int
    return x + 1

# Alternative syntax for many arguments
def send_email(
     address,     # type: Union[str, List[str]]
     sender,      # type: str
     cc,          # type: Optional[List[str]]
     subject='',
     body=None    # type: List[str]
):
    # type: (...) -> bool
    ...
Type comments can’t cause runtime errors because comments aren’t evaluated. They’re never needed in stub files.
String literal types must be defined later in the same module. They can’t resolve cross-module references.

Future annotations import (PEP 563)

Python 3.7+ offers automatic string literal-ification:
from __future__ import annotations

def f(x: A) -> None: ...  # OK, annotation not evaluated

class A: ...
This will likely become the default in Python 3.14+.
With from __future__ import annotations, all function and variable annotations are treated as strings automatically.

Limitations

Even with the future import, some scenarios still need string literals:
Still require string literals or special handling:
  • Type aliases not using the type statement
  • Type narrowing expressions
  • Type definitions (TypeVar, NewType, NamedTuple)
  • Base classes
Base class example:
from __future__ import annotations

class A(tuple['B', 'C']): ...  # String literals still needed
class B: ...
class C: ...

Dynamic evaluation warning

Some libraries evaluate annotations at runtime:
from __future__ import annotations
import typing

def f(x: int | str) -> None: ...  # PEP 604 syntax

# This will fail on Python 3.9:
typing.get_type_hints(f)  # TypeError: unsupported operand type(s)
Be careful with get_type_hints() or eval() on annotations when using newer syntax with older Python versions.

typing.TYPE_CHECKING

The TYPE_CHECKING constant is False at runtime but True during type checking:
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    # Only executed by mypy, not at runtime
    from expensive_module import RareType

def process(x: 'RareType') -> None:
    ...
Code inside if TYPE_CHECKING: is never executed at runtime, making it perfect for type-only imports.

Forward references

Python doesn’t allow class references before definition:
def f(x: A) -> None: ...  # NameError: name "A" is not defined
class A: ...

Solution 1: Future import (Python 3.7+)

from __future__ import annotations

def f(x: A) -> None: ...  # OK
class A: ...

Solution 2: String literal (Python 3.6 and below)

def f(x: 'A') -> None: ...  # OK

# Or type comment
def g(x):  # type: (A) -> None
    ...

class A: ...

Solution 3: Reorder code

Move the class definition before the function (not always possible).

Import cycles

Import cycles occur when module A imports B and B imports A:
ImportError: cannot import name 'b' from partially initialized module 'A'
(most likely due to a circular import)

Solution: TYPE_CHECKING imports

If imports are only needed for annotations:
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import bar

def listify(arg: 'bar.BarClass') -> 'list[bar.BarClass]':
    return [arg]
You must use future import, string literals, or type comments when imports are inside TYPE_CHECKING.

Generic classes at runtime

Some classes are generic in stubs but not at runtime.

Python 3.8 and earlier

Classes like os.PathLike and queue.Queue can’t be subscripted:
from queue import Queue

class Tasks(Queue[str]):  # TypeError: 'type' object is not subscriptable
    ...

results: Queue[int] = Queue()  # TypeError: 'type' object is not subscriptable

Solution 1: Future import (annotations only)

from __future__ import annotations
from queue import Queue

results: Queue[int] = Queue()  # OK

Solution 2: TYPE_CHECKING (inheritance)

from typing import TYPE_CHECKING
from queue import Queue

if TYPE_CHECKING:
    BaseQueue = Queue[str]  # Only seen by mypy
else:
    BaseQueue = Queue  # Used at runtime

class Tasks(BaseQueue):  # OK
    ...

task_queue: Tasks
reveal_type(task_queue.get())  # Reveals str

Generic subclasses

For generic subclasses:
from typing import TYPE_CHECKING, TypeVar, Generic
from queue import Queue

_T = TypeVar("_T")

if TYPE_CHECKING:
    class _MyQueueBase(Queue[_T]): pass
else:
    class _MyQueueBase(Generic[_T], Queue): pass

class MyQueue(_MyQueueBase[_T]): pass

task_queue: MyQueue[str]
reveal_type(task_queue.get())  # Reveals str
Python 3.9+ implements __class_getitem__ on these classes, so direct inheritance works.

Using types from stubs only

Some types only exist in stubs:
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from _typeshed import SupportsRichComparison

def f(x: SupportsRichComparison) -> None:
    ...
The from __future__ import annotations is required to avoid NameError when using the imported symbol.

Generic builtins (Python 3.9+)

Python 3.9 (PEP 585) allows subscripting built-in collections:
from collections.abc import Sequence

x: list[str]  # No need for typing.List
y: dict[int, str]  # No need for typing.Dict
z: Sequence[str] = x

Limited Python 3.7+ support

With from __future__ import annotations, this syntax works in annotations:
from __future__ import annotations

x: list[str]  # OK in annotations only
Be aware this won’t work at runtime on Python 3.7-3.8 if annotations are evaluated.

Union syntax with | (Python 3.10+)

Python 3.10 (PEP 604) allows int | str instead of Union[int, str]:
x: int | str  # Python 3.10+

Limited Python 3.7+ support

Works with future import:
from __future__ import annotations

x: int | str  # OK in annotations, string literals, type comments, stubs
Runtime evaluation on Python 3.7-3.9 will raise:
TypeError: unsupported operand type(s) for |: 'type' and 'type'

New typing module features

Use typing_extensions for newer features on older Python:
from typing_extensions import TypeIs  # Available on all Python versions

Conditional imports

For efficiency, import from typing when available:
import sys

if sys.version_info >= (3, 13):
    from typing import TypeIs
else:
    from typing_extensions import TypeIs
Pair with dependency specification:
typing_extensions; python_version<"3.13"
Always use typing_extensions for the latest typing features across Python versions.

Build docs developers (and LLMs) love