Python Type Hints


  • Description: PEP 484 annotations, built-in generic types (PEP 585), Optional/Union/|, TypeVar generics, Protocol (structural typing), TypedDict, Literal/Final, and how mypy differs 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

  • 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 annotations makes 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
  • Anyobject, Any disables checking; object forces 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]): with from 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; TypedDict itself 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 a class Enum
  • assert_never makes exhaustive match checks detectable

10. Runtime Behavior and TYPE_CHECKING

  • Imports used only in annotations → hide behind TYPE_CHECKING to 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) and pyre (Meta)