Modern Python typing cheatsheet (3.12+ style).

Basic types

x: int = 1
name: str = "Alice"
flag: bool = True
items: list[str] = []
mapping: dict[str, int] = {}
pair: tuple[int, str] = (1, "a")

Use lowercase built-ins: list, dict, tuple, set (no need for typing imports for these).

Union / Optional

# Modern (PEP 604)
x: int | None = None
y: int | str = 1

# Old (still works)
from typing import Optional, Union
x: Optional[int] = None
y: Union[int, str] = 1

Prefer X | Y syntax.

Type aliases (PEP 695)

# Python 3.12+
type UserId = int
type Vector = list[float]

def get_user(id: UserId) -> dict: ...

Old way:

UserId = int    # not a real alias; just a name

Generic functions (PEP 695)

# 3.12+
def first[T](items: list[T]) -> T:
    return items[0]

Old way:

from typing import TypeVar
T = TypeVar("T")

def first(items: list[T]) -> T:
    return items[0]

Generic classes (PEP 695)

class Stack[T]:
    def __init__(self): self._items: list[T] = []
    def push(self, item: T): self._items.append(item)
    def pop(self) -> T: return self._items.pop()

Constraints

def first[T: (int, str)](items: list[T]) -> T:
    return items[0]

T only int or str.

Bounds

class Animal: ...
class Dog(Animal): ...

def name_of[T: Animal](a: T) -> str:
    return a.name

T must be Animal subclass.

Callable

from collections.abc import Callable

def apply(fn: Callable[[int, int], int], x: int, y: int) -> int:
    return fn(x, y)

# Variadic args
fn: Callable[..., int]

Protocol (structural)

from typing import Protocol

class HasName(Protocol):
    name: str

def greet(thing: HasName) -> str:
    return f"hi {thing.name}"

Duck-typing with type checking. No inheritance needed.

TypedDict

from typing import TypedDict

class User(TypedDict):
    id: int
    name: str
    email: str | None

def make_user() -> User:
    return {"id": 1, "name": "Alice", "email": None}

For dict-shaped JSON without BaseModel overhead.

NotRequired / Required

from typing import NotRequired

class User(TypedDict):
    id: int
    name: str
    bio: NotRequired[str]      # optional key

Literal

from typing import Literal

def set_status(s: Literal["active", "inactive", "banned"]) -> None: ...

set_status("active")           # OK
set_status("disabled")         # type error

Final

from typing import Final

MAX_CONN: Final = 100          # can't be reassigned
API_VERSION: Final[str] = "v1"

Self (PEP 673)

from typing import Self

class Builder:
    def with_name(self, n: str) -> Self:
        self.name = n
        return self

Methods that return self.

ParamSpec (decorator typing)

from typing import ParamSpec, Callable
from functools import wraps

P = ParamSpec("P")

def log_calls[T, **P](fn: Callable[P, T]) -> Callable[P, T]:
    @wraps(fn)
    def inner(*args: P.args, **kwargs: P.kwargs) -> T:
        print(f"calling {fn.__name__}")
        return fn(*args, **kwargs)
    return inner

Preserves the wrapped function’s signature.

TypeGuard

from typing import TypeGuard

def is_str_list(items: list[object]) -> TypeGuard[list[str]]:
    return all(isinstance(x, str) for x in items)

def f(items: list[object]):
    if is_str_list(items):
        # items is now list[str] for type checker
        print(items[0].upper())

NewType (branded primitives)

from typing import NewType

UserId = NewType("UserId", int)
PostId = NewType("PostId", int)

def get_user(id: UserId) -> User: ...

get_user(UserId(1))            # OK
get_user(1)                    # type error (still runs at runtime)

Type-safe at compile time; just int at runtime.

Overload

from typing import overload

@overload
def fetch(id: int) -> User: ...
@overload
def fetch(id: str) -> User | None: ...

def fetch(id):
    ...

For type checkers to understand multiple signatures.

Common mistakes

  • List[int] instead of list[int] (still works; less modern).
  • Forgetting from __future__ import annotations for forward refs in old Python.
  • Type aliases without type keyword in 3.12+.
  • Generics with TypeVar when PEP 695 syntax works.

Read this next

If you want my modern Python typing patterns, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .