Python Type Hints Cheat Sheet
Covers basic type annotation syntax, Optional and Union types, generics with TypeVar, and running static checks with mypy.
Basic Type Annotations
Annotate variables, parameters, and return values.
def greet(name: str) -> str: return f"Hello, {name}"age: int = 30price: float = 9.99is_active: bool = Truenames: list[str] = ["Alice", "Bob"] # Python 3.9+scores: dict[str, int] = {"Alice": 90} # Python 3.9+point: tuple[int, int] = (1, 2)def add(a: int, b: int = 0) -> int: return a + b
Optional, Union & None
Express values that may be absent or one of several types.
from typing import Optional, Uniondef find_user(user_id: int) -> Optional[str]: return None # equivalent to Union[str, None]def parse(value: Union[int, str]) -> int: return int(value)# Python 3.10+ shorthanddef find_user_310(user_id: int) -> str | None: return Nonedef parse_310(value: int | str) -> int: return int(value)
Generics & Callable
Write reusable, type-safe containers and higher-order functions.
from typing import TypeVar, Generic, CallableT = TypeVar("T")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()int_stack: Stack[int] = Stack()# Callable[[arg types], return type]def apply(fn: Callable[[int, int], int], a: int, b: int) -> int: return fn(a, b)
Static Checking with mypy
Type hints do nothing at runtime — a checker enforces them.
pip install mypymypy myapp.py # Type-check a single filemypy myapp/ # Type-check a packagemypy --strict myapp.py # Enable all strict checks# type: ignore # Inline comment to silence a specific line
typing Module Toolbox
Frequently used constructs beyond the basics.
- Any- Opts a value out of type checking entirely; use sparingly
- Literal['a', 'b']- Restricts a value to a fixed set of literal values
- TypedDict- Defines a dict with a fixed set of typed string keys
- Protocol- Defines structural typing (duck typing) instead of nominal inheritance
- cast(Type, value)- Tells the type checker to treat value as Type; no runtime effect
- @overload- Declares multiple type signatures for one function based on argument types
- NewType- Creates a distinct type alias to prevent mixing similar primitive types
Protocol: Structural Typing
Type-check by shape (duck typing) instead of requiring explicit inheritance.
from typing import Protocol, runtime_checkable@runtime_checkableclass Closeable(Protocol): def close(self) -> None: ...class FileHandle: def close(self) -> None: print("closed")def shutdown(resource: Closeable) -> None: resource.close()shutdown(FileHandle()) # OK: FileHandle matches the Protocol's shapeprint(isinstance(FileHandle(), Closeable)) # True, thanks to @runtime_checkableclass DataProtocol(Protocol): id: int # Protocols can require attributes too def save(self) -> bool: ...
ParamSpec & Concatenate for Decorators
Preserve a wrapped function's exact parameter signature through a decorator.
from typing import ParamSpec, TypeVar, Callableimport functoolsP = ParamSpec("P")R = TypeVar("R")def with_logging(fn: Callable[P, R]) -> Callable[P, R]: @functools.wraps(fn) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: print(f"calling {fn.__name__}") return fn(*args, **kwargs) return wrapper@with_loggingdef add(a: int, b: int) -> int: return a + b# Type checkers now know add(1, 2) is valid, add("1", 2) is an errorfrom typing import Concatenatedef with_client(fn: Callable[Concatenate["Client", P], R]) -> Callable[P, R]: ... # Inserts a leading Client argument the caller doesn't supply
Self, Final, ClassVar & TypeAlias
Precise annotations for fluent APIs, constants, and shared class state.
from typing import Final, ClassVar, Self, TypeAliasclass QueryBuilder: default_limit: ClassVar[int] = 100 # Shared across instances, not per-instance def where(self, cond: str) -> Self: # Returns the exact subclass type self._conds.append(cond) return self def limit(self, n: int) -> Self: self._limit = n return selfMAX_RETRIES: Final[int] = 3 # Final = reassignment is a type-checker errorUserId: TypeAlias = int # Named alias, improves readabilityUserMap: TypeAlias = dict[UserId, "User"]# type statement (Python 3.12+) is the modern equivalent:# type UserMap = dict[int, "User"]
Variance: Covariant & Contravariant TypeVars
Control how generic subtyping relationships flow through containers.
from typing import TypeVar, GenericT_co = TypeVar("T_co", covariant=True)T_contra = TypeVar("T_contra", contravariant=True)class ReadOnlyBox(Generic[T_co]): def __init__(self, item: T_co) -> None: self._item = item def get(self) -> T_co: return self._item# Because T_co is covariant, ReadOnlyBox[Dog] is a subtype of ReadOnlyBox[Animal]class Handler(Generic[T_contra]): def handle(self, item: T_contra) -> None: ...# Because T_contra is contravariant, Handler[Animal] is a subtype of Handler[Dog]# (a handler that accepts any Animal can safely handle a Dog)
Modern typing Features (3.11+)
Newer constructs for variadic generics and precise dict shapes.
- TypeVarTuple / Unpack- Types variable-length generics like tuples of arbitrary shape, e.g. Array[Unpack[Ts]]
- NotRequired[T] / Required[T]- Marks individual TypedDict keys as optional or mandatory (default flips per total=)
- @override- Marks a subclass method as overriding a parent; checker flags it if the parent signature changes
- assert_type(val, T)- Assertion the type checker verifies statically; no runtime effect
- reveal_type(val)- Debugging aid: checker prints the inferred type at that point
- LiteralString- Restricts a parameter to compile-time string literals, useful for SQL/format-string safety
- Never / NoReturn- Marks a function that never returns normally (always raises or loops forever)
Type hints are not enforced by Python at runtime — they're pure documentation until a tool like mypy or pyright checks them, so wire type checking into CI rather than trusting the hints alone.