Filter concepts by levelShowing all levels.

Python · Core typing

Core typing

Annotating variables, collections, optional values, and the handful of typing-module names that show up in almost every typed codebase

  • name: typedocuments the expected type — Python never checks it
  • list[str] / dict[str, int]built-in generics, no typing import needed since 3.9
  • X | NonePEP 604 spelling of Optional[X], since Python 3.10
  • TypeVar + Genericties a placeholder type across a signature or class

1.Basic annotations

A type annotation writes the expected type after a colon — name: type for a variable, param: type for an argument, -> type for a return value. Python stores it but never checks it while running.

Key features

  • name: type annotates a variable; param: type annotates a function argument
  • -> type after the parameter list annotates the return value
  • Annotations are optional and have zero effect on how the code actually runs
  • A type checker (mypy, pyright) reads annotations; the interpreter does not enforce them
  • Available on variables, function parameters, and return values since Python 3.5 (PEP 484)
Where an annotation can go

x: int

variable annotation

def f(x: int)

parameter annotation

-> str:

return annotation

  1. x: int — variable annotation
  2. def f(x: int) — parameter annotation
  3. -> str: — return annotation

Command

python
age: int = 5

def greet(name: str, times: int = 1) -> str:
    return (name + " ") * times

name: type / param: type / -> type documents the expected type. Python stores it and never checks it while running.

2.Generic collection annotations

list[str], dict[str, int], tuple[str, int], and set[str] annotate a collection AND the type of what it holds. Since Python 3.9 these built-ins work directly — no typing.List import needed.

Key features

  • list[str] = a list expected to hold only str items
  • dict[str, int] = a dict expected to map str keys to int values
  • tuple[str, int] = a FIXED-length, fixed-position tuple — first str, then int
  • set[str] = a set expected to hold only str items
  • Built-ins work directly since Python 3.9 (PEP 585) — typing.List etc. are legacy now

The four container generics and what each position means

The four container generics and what each position means
AnnotationMeaning
list[str]a list; every element is a str
dict[str, int]a dict; every key is str, every value is int
tuple[str, int]a fixed 2-tuple; position 0 is str, position 1 is int
set[str]a set; every element is a str
Reading a container generic, position by position

scores: dict[str, int] = {"Ana": 90}

dict

the built-in itself — generic-subscriptable directly since PEP 585 — no typing.Dict import

str

key type — every key must be a str

int

value type — every value must be an int

  • Whole: scores: dict[str, int] = {"Ana": 90}
  • dict — the built-in itself: generic-subscriptable directly since PEP 585 — no typing.Dict import
  • str — key type: every key must be a str
  • int — value type: every value must be an int

Command

python
names: list[str]
scores: dict[str, int]
point: tuple[str, int]
tags: set[str]

list[str], dict[str,int], tuple[str,int], set[str] work directly since Python 3.9 — tuple[str,int] is a FIXED 2-tuple, not "any length."

3.Optional and Union types

Optional[X] means "X or None." Union[X, Y] means "X or Y." Since Python 3.10, X | None and X | Y are the shorter, preferred spelling for the same thing (PEP 604).

Key features

  • Optional[X] is shorthand for Union[X, None] — "X or None," nothing else
  • Union[X, Y] means the value is X or Y, and a type checker requires handling both
  • X | None and X | Y (PEP 604) replace Optional/Union since Python 3.10 — same meaning
  • Optional does NOT mean "this parameter may be omitted" — a default value does that
  • A function returning X | None forces a caller's checker to handle the None case
get_user returns a User, or nothing

get_user(id)

looks up the id

User | None

found -> User, missing -> None

caller checks

must handle both branches

  1. get_user(id) — looks up the id
  2. User | None — found -> User, missing -> None
  3. caller checks — must handle both branches

Command

python
from typing import Optional, Union

def find_user(uid: int) -> Optional[str]: ...      # str or None
def parse(value: Union[int, str]) -> str: ...       # int or str

# modern spelling (Python 3.10+), same meaning:
def find_user2(uid: int) -> str | None: ...
def parse2(value: int | str) -> str: ...

Optional[X] = X | None. Union[X, Y] = X | Y. Optional never means "the argument can be omitted" — a default value does that.

4.Any, Literal, and Final

Any turns off type checking for a value entirely. Literal["r", "w"] restricts a value to specific literal options. Final marks a name as meant to never be reassigned. None of the three are enforced while the code runs.

Key features

  • Any accepts anything and disables type checking for that value entirely
  • Literal["r", "w", "a"] restricts a value to exactly those literal options
  • Final marks a name as intended to never be reassigned after its first value
  • A type checker enforces Literal and Final; Python enforces neither at runtime
  • Overusing Any silently defeats the purpose of adding type hints in the first place

Command

python
from typing import Any, Literal, Final

def handle(payload: Any) -> None: ...        # anything goes, unchecked

Mode = Literal["r", "w", "a"]
def open_file(path: str, mode: Mode) -> str: ...

MAX_RETRIES: Final = 3                        # not meant to change

Any disables checking; Literal restricts to specific values; Final marks "should not change." A type checker enforces all three — Python itself enforces none.

5.Callable

Callable[[ArgTypes], ReturnType] annotates a value that is itself a function — a callback parameter, for example. Callable[[int, int], int] means "a function taking two ints, returning an int."

Key features

  • Callable[[ArgTypes], ReturnType] — the argument types go in a list, then the return type
  • Callable[..., ReturnType] (literal Ellipsis) means "any arguments, this return type"
  • Works for a plain function, a lambda, or any object with a matching __call__
  • A type checker verifies the callback signature; Python does not check it at call time
  • Common for a callback parameter — "give me a function that does X"

Command

python
from typing import Callable

def apply(fn: Callable[[int, int], int], a: int, b: int) -> int:
    return fn(a, b)

Callable[[int, int], int] means "a function taking two ints, returning an int." A type checker verifies the match; Python just calls it.

6.TypeVar and Generic

TypeVar("T") creates a placeholder type name. Generic[T] lets a class be parametrized over it, so Box[int] and Box[str] are both Box but each remembers what it holds.

Key features

  • T = TypeVar("T") declares a placeholder type name, conventionally a single capital letter
  • class Box(Generic[T]): makes Box parametrizable — Box[int]/Box[str] become tracked types
  • The SAME TypeVar used twice in a signature ties both occurrences to one concrete type
  • A type checker infers T from what is passed in — you rarely write Box[int] explicitly
  • Generic underlies the built-in container generics (list[T], dict[K, V]) too
One shape, two concrete fillings

Box(Generic[T])

the reusable shape

Box[int]

T filled with int

Box[str]

T filled with str

  1. Box(Generic[T]) — the reusable shape
  2. Box[int] — T filled with int
  3. Box[str] — T filled with str

Command

python
from typing import TypeVar, Generic

T = TypeVar("T")

class Box(Generic[T]):
    def __init__(self, item: T) -> None:
        self.item = item
    def get(self) -> T:
        return self.item

TypeVar("T") is a placeholder type; Generic[T] lets a class be parametrized over it — Box[int] and Box[str] are tracked as distinct.

7.TypeAlias and Annotated

UserId: TypeAlias = int gives int a second, more meaningful name for use in annotations. Annotated[int, "must be > 0"] attaches extra metadata to a type without changing what the type itself means.

Key features

  • UserId: TypeAlias = int declares UserId as another name for int, explicitly
  • A type alias changes nothing at runtime — UserId and int are interchangeable, always
  • Annotated[int, extra_data] is still exactly an int to a type checker's type rules
  • Annotated's extra metadata is ignored unless a specific tool is written to read it
  • FastAPI/Pydantic use Annotated to attach validation rules a type checker does not enforce

Command

python
from typing import TypeAlias, Annotated

UserId: TypeAlias = int

def lookup(uid: UserId) -> str: ...

PositiveInt = Annotated[int, "must be > 0"]
def set_age(age: PositiveInt) -> None: ...

TypeAlias names a type for reuse; Annotated attaches metadata a tool can read. Neither is enforced by Python or by most type checkers alone.

Remember

  • An annotation documents a type; it never stops the "wrong" value from being passed or returned.
  • tuple[str, int] is a FIXED 2-tuple — use tuple[int, ...] for "any length, same type."
  • Optional[X] means X or None — it never means "this argument can be omitted."
  • Any disables checking entirely; reach for it rarely, and prefer a precise Union instead.

Pro tip

  • Prefer X | None and X | Y (PEP 604) over Optional[X]/Union[X, Y] on Python 3.10+ — shorter, no import.
  • list[str], dict[str, int] etc. work directly since Python 3.9 — typing.List/Dict are legacy spellings.
  • A TypeVar only links occurrences within ONE signature or class — reusing it elsewhere ties nothing together.
  • Annotated metadata is inert to plain Python and most type checkers — only a framework built to read it enforces it.

Next up

  • Advanced typing
  • Tools

Sources

  • typing — docs.python.org/3/library/typing.html
  • PEP 585 (generic built-ins) — peps.python.org/pep-0585
  • Every example executed on Python 3.14.3 · checked 2026-08-21

Spotted a mistake on this page? Tell us.