Skip to main content

Class types

Every class is also a valid type. Any instance of a subclass is compatible with all superclasses:
class A:
    def f(self) -> int:
        return 2

class B(A):
    def f(self) -> int:
        return 3
    def g(self) -> int:
        return 4

def foo(a: A) -> None:
    print(a.f())  # OK
    a.g()         # Error: "A" has no attribute "g"

foo(B())  # OK (B is a subclass of A)

The Any type

A value with the Any type is dynamically typed. Mypy doesn’t know anything about the possible runtime types of such value.
from typing import Any

a: Any = None
s: str = ''
a = 2     # OK (assign "int" to "Any")
s = a     # OK (assign "Any" to "str")
You need to be careful with Any types, since they let you lie to mypy. This could easily hide bugs.

Any vs object

Any should not be confused with object. Unlike object, Any introduces type unsafety:
  • Any is compatible with every type, and vice versa
  • object is compatible with every type, but other types are not compatible with object
def show_heading(s: object) -> None:
    print('=== ' + s + ' ===')  # Error: can't concatenate object and str

show_heading(1)  # OK (runtime error only)

Tuple types

The type tuple[T1, ..., Tn] represents a tuple with specific item types:
def f(t: tuple[int, str]) -> None:
    t = 1, 'foo'    # OK
    t = 'foo', 1    # Error: Type check error
Usually it’s better to use Sequence[T] instead of tuple[T, ...], as Sequence is also compatible with lists and other non-tuple sequences.

Callable types

You can pass around function objects and bound methods in statically typed code. The type of a function is Callable[[A1, ..., An], Rt]:
from collections.abc import Callable

def twice(i: int, next: Callable[[int], int]) -> int:
    return next(next(i))

def add(i: int) -> int:
    return i + 1

print(twice(3, add))   # 5

Flexible callable types

You can use Callable[..., T] for less typical cases:
from collections.abc import Callable

def arbitrary_call(f: Callable[..., int]) -> int:
    return f('x') + f(y=2)  # OK

arbitrary_call(ord)   # No static error, but fails at runtime

Union types

Python functions often accept values of two or more different types. Use T1 | T2 | ... | Tn to construct a union type:
def f(x: int | str) -> None:
    x + 1     # Error: str + int is not valid
    if isinstance(x, int):
        # Here type of x is int
        x + 1      # OK
    else:
        # Here type of x is str
        x + 'a'    # OK

f(1)    # OK
f('x')  # OK
f(1.1)  # Error
Operations are valid for union types only if they are valid for every union item. This is why isinstance checks are often necessary.

Legacy union syntax

For Python 3.9 and older, use Union[T1, T2, ...]:
from typing import Union

def f(x: Union[int, str]) -> None:
    ...

Optional types and None

You can use T | None to define a type variant that allows None:
def strlen(s: str) -> int | None:
    if not s:
        return None  # OK
    return len(s)
def my_inc(x: int | None) -> int:
    if x is None:
        return 0
    else:
        return x + 1

None checks

Mypy recognizes several None checks:
if x is None:
    return 0
else:
    return x + 1  # x is int here

Type aliases

Type names can be long. You can define a type alias by assigning the type to a variable:
type AliasType = list[dict[tuple[int, str], set[int]]] | tuple[str, list[str]]

def f() -> AliasType:
    ...
A type alias doesn’t create a new type. It’s just a shorthand notation for another type.

Named tuples

Mypy recognizes named tuples and can type check code that uses them:
from typing import NamedTuple

class Point(NamedTuple):
    x: int
    y: int

p = Point(x=1, y='x')  # Error: Argument has incompatible type "str"

The type of class objects

Use type[C] to refer to a class object deriving from C:
class User:
    pass

class BasicUser(User):
    def upgrade(self):
        """Upgrade to Pro"""

class ProUser(User):
    def pay(self):
        """Pay bill"""

def new_user[U: User](user_class: type[U]) -> U:
    return user_class()

beginner = new_user(BasicUser)  # Inferred type is BasicUser
beginner.upgrade()  # OK
The value corresponding to type[C] must be an actual class object that’s a subtype of C.

Generators

A basic generator that only yields values can be annotated as Iterator[YieldType] or Iterable[YieldType]:
from typing import Iterator

def squares(n: int) -> Iterator[int]:
    for i in range(n):
        yield i * i
If your generator accepts values via send() or returns a value, use Generator[YieldType, SendType, ReturnType]:
from typing import Generator

def echo_round() -> Generator[int, float, str]:
    sent = yield 0
    while sent >= 0:
        sent = yield round(sent)
    return 'Done'

Build docs developers (and LLMs) love