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 oflist[int](still works; less modern).- Forgetting
from __future__ import annotationsfor forward refs in old Python. - Type aliases without
typekeyword 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 .