Python Type Hints
- Description: PEP 484 annotations, built-in generic types (PEP 585),
Optional/Union/|,TypeVargenerics,Protocol(structural typing),TypedDict,Literal/Final, and howmypydiffers from runtime - My Notion Note ID: K2A-D1-9
- Created: 2023-05-15
- Updated: 2026-05-11
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Why Type Hints
- 2. Variable and Function Annotations
- 3. Built-In Generic Types
- 4.
Optional,Union, and| - 5.
Any,Callable,TypeAlias,Final,ClassVar - 6. Generics with
TypeVar - 7.
Protocol: Structural Typing - 8.
TypedDict - 9.
Literalandassert_never - 10. Runtime Behavior and
TYPE_CHECKING - 11.
mypyQuick Start
1. Why Type Hints
- PEP 484 (since 3.5), optional annotations
- Consumed by static checkers:
mypy,pyright,pyre - Not enforced at runtime: Python remains dynamically typed
Two reasons to add them:
- Catch bugs at edit time instead of in production
- Self-documenting signatures; IDEs use them for completion and inline docs
2. Variable and Function Annotations
name: str = "Yu"
count: int # bare annotation (no value)
ratio: float = 0.5
def parse(text: str, *, strict: bool = False) -> dict[str, int]:
...
class User:
id: int # class-level → instance attr annotation
name: str
- Annotations live in
__annotations__ - They are expressions (undefined names fail at import time), unless
from __future__ import annotationsmakes them all strings
3. Built-In Generic Types
- PEP 585 (3.9+), parametrize built-in containers directly
- Use lowercase forms;
typing.List,typing.Dict, etc. are deprecated
nums: list[int]
labels: dict[str, list[int]]
pair: tuple[str, int]
unique: set[str]
- For Python < 3.9, import from
typing:List[int],Dict[str, int]
4. Optional, Union, and |
# Old style
from typing import Optional, Union
def find(uid: int) -> Optional[User]: ...
def serialize(x: Union[int, str]) -> bytes: ...
# 3.10+, PEP 604
def find(uid: int) -> User | None: ...
def serialize(x: int | str) -> bytes: ...
Optional[T]≡T | None- Prefer
| None, composes with other unions, reads naturally
5. Any, Callable, TypeAlias, Final, ClassVar
from typing import Any, Callable, Final, ClassVar
from typing import TypeAlias # 3.10+; 3.12+ has `type X = ...`
x: Any # opt out of checking, use sparingly
cb: Callable[[int, int], int] # (int, int) -> int
cb_any: Callable[..., bool] # any args, returns bool
JSON: TypeAlias = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None
MAX: Final = 100 # cannot be rebound
class Counter:
instances: ClassVar[int] = 0 # class attribute, not instance
Any≠object,Anydisables checking;objectforces casts
6. Generics with TypeVar
from typing import TypeVar
T = TypeVar("T")
N = TypeVar("N", int, float) # constrained to int or float
def first(xs: list[T]) -> T:
return xs[0]
def total(xs: list[N]) -> N:
return sum(xs) # type: ignore[return-value]
# Generic class (3.12+ PEP 695 syntax)
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, x: T) -> None: self._items.append(x)
def pop(self) -> T: return self._items.pop()
- For Python < 3.12, write
class Stack(Generic[T]):withfrom typing import Generic
7. Protocol: Structural Typing
- PEP 544 (3.8+), duck typing for the type checker
- "Anything with these methods qualifies"; no
class C(HasLen):inheritance needed
from typing import Protocol
class HasLen(Protocol):
def __len__(self) -> int: ...
def is_empty(x: HasLen) -> bool:
return len(x) == 0
is_empty([1, 2, 3]) # OK, list has __len__
is_empty("abc") # OK, str has __len__
- Closest to C++20
concepts, but for types not templates
8. TypedDict
- For dicts with a fixed schema (typical for JSON-shaped data)
from typing import TypedDict, NotRequired
class UserDoc(TypedDict):
id: int
name: str
email: NotRequired[str] # optional key (3.11+)
u: UserDoc = {"id": 1, "name": "Yu"}
- Runtime checks live in
pydantic/dataclasses;TypedDictitself is checker-only
9. Literal and assert_never
from typing import Literal, assert_never
Color = Literal["red", "green", "blue"]
def hex_for(c: Color) -> str:
match c:
case "red": return "#f00"
case "green": return "#0f0"
case "blue": return "#00f"
case _:
assert_never(c) # 3.11+; checker errors if a case is missing
Literal[...]describes enumerations of values without aclass Enumassert_nevermakes exhaustivematchchecks detectable
10. Runtime Behavior and TYPE_CHECKING
- Imports used only in annotations → hide behind
TYPE_CHECKINGto break import cycles and skip cost
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .models import User # not imported at runtime
def lookup(uid: int) -> "User": # forward reference as string
...
from __future__ import annotations(3.7+), every annotation becomes a string; no need to quote forward references
11. mypy Quick Start
pip install mypy
mypy your_package/
mypy --strict your_package/ # turn every check on
- Inline opt-out:
# type: ignore[error-code] - Project config in
pyproject.toml:
[tool.mypy]
strict = true
warn_unused_ignores = true
- Alternatives:
pyright(Microsoft, faster, used inside Pylance) andpyre(Meta)