Skip to content

Type Hints and Annotations#

Type hints annotate expected types for parameters, return values, variables, and attributes. They are stored in __annotations__ but are not enforced at runtime by default — static checkers (mypy, pyright, Pylance) and IDEs use them for verification and autocomplete. Python 3.10+ modernized syntax with X | Y unions, match integration, and PEP 695-style generics in 3.12.

How to use this page

Start with function signatures, then learn Protocol, TypedDict, and generics. Foundation: Function Annotations and Type Hints. Pair with Unit Testing for typed test doubles.

At a glance
Track Python Advanced → Typing and Static Analysis
Sections 13 major topics
Outline Use the right-hand TOC to jump

Topics: Annotations vs runtime behavior · Modern syntax (Python 3.10+) · Variables and attributes · TypedDict — structured dicts · Protocol — structural subtyping · Generics · Literal, Final, and Annotated · Self (Python 3.11+) · … (+5 more)

  1. Annotations vs runtime behavior
  2. Modern syntax (Python 3.10+)
  3. Variables and attributes
  4. TypedDict — structured dicts
  5. Protocol — structural subtyping
  6. Generics
  7. Literal, Final, and Annotated
  8. Self (Python 3.11+)
  9. TypeAlias and NewType
  10. Overloads
  11. TYPE_CHECKING guard
  12. Configuration for strict projects … and 1 more sections

Annotations vs runtime behavior#

def add(a: int, b: int) -> int:
    return a + b

add("hello", "world")  # No TypeError at runtime — returns "helloworld"
print(add.__annotations__)
# {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
Tool Role
mypy De facto static checker; strict mode catches most bugs
pyright / Pylance Fast checker bundled with VS Code
pytype Google's inference-heavy checker
@runtime_checkable Opt-in isinstance for Protocol (limited)
pip install mypy
mypy src/ --strict

Modern syntax (Python 3.10+)#

Union with |#

# Python 3.10+
def parse(value: str | int) -> int:
    return int(value)

# Pre-3.10 equivalent
from typing import Union
def parse_legacy(value: Union[str, int]) -> int:
    return int(value)

Optional#

def find_user(user_id: int) -> dict[str, str] | None:
    ...

Optional[T] is equivalent to T | None — prefer T | None in new code.

Built-in generics (3.9+)#

def tally(items: list[str]) -> dict[str, int]:
    counts: dict[str, int] = {}
    for item in items:
        counts[item] = counts.get(item, 0) + 1
    return counts

Use list, dict, set, tuple directly — no need for typing.List in 3.9+.


Variables and attributes#

from typing import ClassVar

class Config:
    DEFAULT_TIMEOUT: ClassVar[float] = 30.0
    timeout: float

    def __init__(self, timeout: float = DEFAULT_TIMEOUT) -> None:
        self.timeout = timeout

name: str = "app"
count: int | None = None
Annotation target Stored in
Function params / return func.__annotations__
Module-level variables __annotations__ on module
Class attributes cls.__annotations__

TypedDict — structured dicts#

from typing import TypedDict, Required, NotRequired

class User(TypedDict):
    id: int
    name: str
    email: NotRequired[str]  # 3.11+

def greet(user: User) -> str:
    return f"Hello, {user['name']}"

data: User = {"id": 1, "name": "Ada"}

Use TypedDict for JSON-shaped dicts instead of untyped dict[str, Any].


Protocol — structural subtyping#

from typing import Protocol

class SupportsClose(Protocol):
    def close(self) -> None: ...

def shutdown(resource: SupportsClose) -> None:
    resource.close()

class FileWrapper:
    def close(self) -> None:
        print("closed")

shutdown(FileWrapper())  # OK — has close(), no inheritance required

Protocols enable duck typing with static verification. Use @runtime_checkable only when you need isinstance checks.


Generics#

TypeVar (classic)#

from typing import TypeVar, Generic

T = TypeVar("T")

def first(items: list[T]) -> T:
    return items[0]

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()

PEP 695 syntax (Python 3.12+)#

def first[T](items: list[T]) -> T:
    return items[0]

class Stack[T]:
    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

Literal, Final, and Annotated#

from typing import Literal, Final, Annotated

Mode = Literal["r", "w", "a"]

def open_mode(mode: Mode) -> None:
    ...

MAX_RETRIES: Final = 3
MAX_RETRIES = 4  # mypy error — cannot reassign Final

UserId = Annotated[int, "positive integer user id"]

def fetch(user_id: UserId) -> dict:
    ...

Annotated attaches metadata for frameworks (Pydantic, FastAPI) without changing the base type.


Self (Python 3.11+)#

from typing import Self

class Builder:
    def with_name(self, name: str) -> Self:
        self.name = name
        return self

    def build(self) -> "Product":
        ...

Self correctly types fluent APIs returning the current class (including subclasses).


TypeAlias and NewType#

from typing import TypeAlias, NewType

JsonDict: TypeAlias = dict[str, "JsonValue"]
JsonValue: TypeAlias = str | int | float | bool | None | JsonDict | list["JsonValue"]

UserId = NewType("UserId", int)

def get_user(uid: UserId) -> dict:
    ...

raw = 42
get_user(UserId(raw))   # OK
get_user(raw)           # mypy error — int is not UserId

NewType creates a distinct type for the checker; at runtime it is still int.


Overloads#

from typing import overload

@overload
def process(data: str) -> str: ...

@overload
def process(data: bytes) -> bytes: ...

def process(data: str | bytes) -> str | bytes:
    if isinstance(data, str):
        return data.upper()
    return data.upper()  # bytes.upper()

Overloads document different return types for different input types — only the final implementation runs at runtime.


TYPE_CHECKING guard#

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Iterable  # import only for type checkers

def consume(items: "Iterable[str]") -> None:
    for item in items:
        print(item)

Avoids circular imports and heavy runtime import costs.


Configuration for strict projects#

# pyproject.toml
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
disallow_untyped_defs = true

[[tool.mypy.overrides]]
module = "third_party.*"
ignore_missing_imports = true
Flag Effect
strict = true Enables full strictness bundle
disallow_any_generics No bare list without type params
no_implicit_optional def f(x: int = None) is an error

Common patterns in production code#

from collections.abc import Callable, Iterator, Sequence
from pathlib import Path

def read_lines(path: Path) -> Iterator[str]:
    with path.open(encoding="utf-8") as f:
        yield from f

def apply_twice(fn: Callable[[int], int], value: int) -> int:
    return fn(fn(value))

def average(nums: Sequence[float]) -> float:
    return sum(nums) / len(nums)

Prefer collections.abc abstract types over concrete list/dict in parameter positions — callers can pass any compatible sequence/mapping.


Interview traps (quick reference)#

Trap What goes wrong Safe approach
"Type hints enforce types at runtime" No TypeError without extra tools Use mypy/pyright in CI
list vs List in 3.9+ Outdated style Built-in generics
Optional[str] meaning Forgetting None is valid str \| None explicitly
Mutable default with typed param Same runtime bug as untyped None sentinel pattern
Any everywhere Defeats purpose of hints Narrow types progressively
TypedDict with wrong keys Runtime KeyError still possible Validate at boundaries
Ignoring Protocol Over-abstract ABC inheritance Structural typing for duck types

Mental model checklist#

  1. Where are annotations stored at runtime?
  2. What is the difference between Union[str, int] and str | int?
  3. When should you use Protocol instead of an ABC?
  4. What does Self solve in class method chaining?
  5. Why use TYPE_CHECKING imports?
  6. How does NewType differ from TypeAlias?

What's next#

Topic Page
Function-level intro Function Annotations and Type Hints
New 3.10+ syntax New Syntax and Features
Testing typed code Unit Testing
CLI with types Building Command Line Interfaces