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.

OldCurrentSince
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 | None3.10
Union[int, str]int | str3.10
typing.Callablecollections.abc.Callable3.9
typing.Iterablecollections.abc.Iterable3.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.

AnnotationAccepts
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, Iteratorsingle-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.