Python Cheatsheet
Type Hints
Use this Python reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Modern Syntax First
Annotations are hints. Nothing is enforced at runtime, def f(n: int) still
accepts a string. A checker (mypy, pyright) is what turns them into errors.
Write the current forms, not the 2018 ones. Everything on the left is deprecated in favor of the right.
| Old | Current | Since |
|---|---|---|
typing.List[int] | list[int] | 3.9 |
typing.Dict[str, int] | dict[str, int] | 3.9 |
typing.Tuple[int, ...] | tuple[int, ...] | 3.9 |
typing.Type[Foo] | type[Foo] | 3.9 |
Optional[str] | str | None | 3.10 |
Union[int, str] | int | str | 3.10 |
typing.Callable | collections.abc.Callable | 3.9 |
typing.Iterable | collections.abc.Iterable | 3.9 |
def parse(raw: str, *, strict: bool = False) -> dict[str, int | None]: ... names: list[str] = [] counts: dict[str, int] = {} pair: tuple[str, int] = ("a", 1) # exactly two items row: tuple[int, ...] = (1, 2, 3) # any length, all int maybe: str | None = None handler: Callable[[str, int], bool] nested: dict[str, list[tuple[int, str]]] = {}
Type Parameters (3.12+)
PEP 695 gives functions, classes, and aliases their own parameter list, so most
code never needs an explicit TypeVar again. PEP 696 adds defaults in 3.13.
def first[T](items: Sequence[T]) -> T: # 3.12+ return items[0] class Box[T]: # generic class, no Generic[T] def __init__(self, item: T) -> None: self.item = item def get(self) -> T: return self.item def clamp[T: (int, float)](x: T, lo: T, hi: T) -> T: # constrained return max(lo, min(x, hi)) def key_of[K, V](d: Mapping[K, V], value: V) -> K | None: ... def load[T = dict](raw: str) -> T: # 3.13, default type argument ... type Point = tuple[float, float] # 3.12 alias statement, lazy type Json = str | int | float | bool | None | list[Json] | dict[str, Json] type Pair[T] = tuple[T, T] # generic alias
type X = ... builds a TypeAliasType whose right side is evaluated lazily, so a
recursive alias like Json needs no quotes. Point.__value__ gives the target.
Generics the Explicit Way
Still needed on 3.11 and older, and still what you read in library source.
from typing import TypeVar, Generic from collections.abc import Sequence T = TypeVar("T") # unconstrained S = TypeVar("S", bound=Sequence[int]) # any subtype of Sequence[int] N = TypeVar("N", int, float) # exactly int or float T_co = TypeVar("T_co", covariant=True) # read-only position T_contra = TypeVar("T_contra", contravariant=True) class Stack(Generic[T]): def __init__(self) -> None: self._items: list[T] = [] def push(self, item: T) -> None: self._items.append(item) def pop(self) -> T: return self._items.pop() s: Stack[int] = Stack()
Variance in one line: covariant means Container[Dog] is usable as
Container[Animal] (safe for read-only, like Sequence), contravariant is the
reverse (callback arguments), invariant is neither (list, because you can write
to it). Under PEP 695 syntax the checker infers variance for you.
Be Abstract in Arguments, Concrete in Returns
Accept the least you need from collections.abc, return the exact type you built.
Annotating a parameter list[str] rejects a tuple, a generator, and a dict
view for no reason.
| Annotation | Accepts |
|---|---|
Iterable[T] | anything you can for over once, generators included |
Iterator[T] | has __next__, one-shot |
Collection[T] | iterable, sized, supports in |
Sequence[T] | indexable and len, so list, tuple, str |
MutableSequence[T] | Sequence plus append and item assignment |
Mapping[K, V] | read-only dict-like |
MutableMapping[K, V] | Mapping plus __setitem__ and del |
Set[T] / MutableSet[T] | abstract set, not the builtin set |
Callable[[int, str], bool] | function taking (int, str), returning bool |
Callable[..., None] | any signature |
Hashable, Sized, Iterator | single-method protocols |
from collections.abc import Iterable, Mapping, Callable, Iterator def total(values: Iterable[float]) -> float: # takes list, tuple, genexp return sum(values) def lookup(cfg: Mapping[str, str], key: str) -> str | None: return cfg.get(key) def chunk(items: Iterable[T], n: int) -> Iterator[list[T]]: # generator returns Iterator ...
A generator function is annotated by what it returns: Iterator[T] for a plain
one, Generator[YieldT, SendT, ReturnT] when send or the return value matters.
Any, object, Never, Literal, Final, ClassVar
from typing import Any, Never, NoReturn, Literal, Final, ClassVar, LiteralString def loose(x: Any) -> Any: ... # opts OUT of checking, silently allows all def strict(x: object) -> None: # accepts anything, allows NOTHING on it if isinstance(x, str): # must narrow before use print(x.upper()) def die(msg: str) -> Never: # 3.11, never returns normally raise RuntimeError(msg) def serve() -> NoReturn: # same idea, older spelling while True: ... Mode = Literal["r", "w", "a"] # only these three strings def openf(path: str, mode: Mode = "r") -> None: ... def flag(on: Literal[True]) -> None: ... MAX_RETRIES: Final = 3 # reassignment is a checker error TIMEOUT: Final[float] = 5.0 class Config: registry: ClassVar[dict[str, str]] = {} # class attribute, not per instance name: str # instance attribute
Any is the escape hatch and it is contagious, every attribute of an Any is
Any. Prefer object plus narrowing when you genuinely accept anything.
TypedDict and NamedTuple
TypedDict types a plain dict, with no runtime class and no validation.
NamedTuple is a real immutable tuple subclass.
from typing import TypedDict, NotRequired, Required, ReadOnly, NamedTuple class Movie(TypedDict): title: str year: NotRequired[int] # 3.11, per-key optional slug: ReadOnly[str] # 3.13, cannot be reassigned class Partial(TypedDict, total=False): a: int b: Required[str] # required inside a total=False dict m: Movie = {"title": "Alien", "slug": "alien"} Movie.__required_keys__ # frozenset({'title', 'slug'}) Movie.__optional_keys__ # frozenset({'year'}) class Point(NamedTuple): x: int y: int = 0 # defaults must come last p = Point(1) # Point(x=1, y=0) p.x, p[0] # both work p._replace(y=5) # Point(x=1, y=5) p._asdict() # {'x': 1, 'y': 0}
Protocol for Structural Typing
Duck typing with a checker. The implementer needs no import, no inheritance, and no registration, it just has to have the members.
from typing import Protocol, runtime_checkable, Self import io class SupportsWrite(Protocol): def write(self, s: str, /) -> int: ... def emit(sink: SupportsWrite, msg: str) -> None: sink.write(msg + "\n") emit(open("out.txt", "w"), "hi") # files match, no relationship declared emit(io.StringIO(), "hi") @runtime_checkable # enables isinstance, METHOD NAMES ONLY class Closable(Protocol): def close(self) -> None: ... isinstance(some_file, Closable) # True, signatures are NOT checked class Comparable(Protocol): def __lt__(self, other: Self, /) -> bool: ...
overload, cast, Narrowing
from typing import overload, cast, assert_type, TypeGuard, TypeIs, Self @overload def get(key: str) -> str | None: ... @overload def get(key: str, default: str) -> str: ... def get(key: str, default: str | None = None) -> str | None: # the only real body return store.get(key, default) raw = cast(dict[str, int], json.loads(text)) # asserts a type, zero runtime cost assert_type(raw, dict[str, int]) # 3.11, fails the CHECK if wrong def is_strs(v: list[object]) -> TypeIs[list[str]]: # 3.13, narrows BOTH branches return all(isinstance(x, str) for x in v) def is_strs_old(v: list[object]) -> TypeGuard[list[str]]: # 3.10, True branch only return all(isinstance(x, str) for x in v) class Query: def where(self, cond: str) -> Self: # 3.11, subclasses return themselves return self
Narrowing a checker understands for free: isinstance, is None,
type(x) is T, a Literal equality check, assert, and an early return.
TypeIs is the one to reach for now, it narrows the negative branch too and its
type must be a subtype of the declared parameter. TypeGuard does not.
Decorators, Variadics, Annotated
from typing import ParamSpec, TypeVar, Concatenate, Unpack, TypedDict, Annotated from collections.abc import Callable import functools P = ParamSpec("P") # 3.10, captures a full signature R = TypeVar("R") def logged(fn: Callable[P, R]) -> Callable[P, R]: # signature preserved @functools.wraps(fn) def inner(*args: P.args, **kwargs: P.kwargs) -> R: return fn(*args, **kwargs) return inner def with_conn(fn: Callable[Concatenate[Conn, P], R]) -> Callable[P, R]: ... # Concatenate adds or strips leading args, here the injected Conn def stats[*Ts](*args: *Ts) -> tuple[*Ts]: # 3.11 TypeVarTuple return args class Opts(TypedDict): retries: int timeout: float def fetch(url: str, **kwargs: Unpack[Opts]) -> None: # 3.11, typed **kwargs ... Age = Annotated[int, "years"] # 3.9, extra metadata for other tools Port = Annotated[int, Field(gt=0, lt=65536)] # pydantic Field, FastAPI reads it get_type_hints(f) # strips Annotated metadata get_type_hints(f, include_extras=True) # keeps it
With PEP 695 syntax the same decorator is def logged[**P, R](fn: Callable[P, R]).
Forward References and TYPE_CHECKING
A name used before it exists goes in quotes, or you turn on lazy annotations.
from __future__ import annotations # every annotation becomes a string from typing import TYPE_CHECKING if TYPE_CHECKING: # import only for the checker, no cycle from myapp.models import User # and no runtime import cost class Node: def __init__(self, parent: Node | None = None) -> None: # no quotes needed self.parent = parent def sibling(self) -> "Node": # quotes always work ... def save(user: User) -> None: # fine, the annotation is never evaluated ...
Costs of from __future__ import annotations: anything that reads annotations at
runtime (pydantic, dataclasses with InitVar, get_type_hints) must resolve the
strings, and it fails if the name is only imported under TYPE_CHECKING. Pass
localns= to get_type_hints, or drop the future import in those modules.
Running a Checker
python -m pip install mypy mypy --strict src/ # the setting that actually finds bugs mypy --ignore-missing-imports app.py # third-party libs without stubs python -m pip install types-requests # stub package for a library python -m pip install pyright pyright # faster, stricter inference, VS Code default pyright --outputjson
# pyproject.toml [tool.mypy] python_version = "3.13" strict = true warn_unreachable = true disallow_untyped_defs = true [tool.pyright] typeCheckingMode = "strict"
Silence a single line with # type: ignore[arg-type] (mypy) or
# pyright: ignore[reportArgumentType], always with the code so it does not hide
the next error. --strict mainly turns on "every function must be annotated" and
"no implicit Any". Add it to a new project on day one, and to an old one
directory by directory.