Filter concepts by levelShowing all levels.

Python · Advanced typing

Advanced typing

A dict with a known shape, narrowing a Union down after a check, writing your own generic code, and the substitution rule behind it all

  • TypedDicta dict with fixed keys and a type per key — still a plain dict
  • isinstance narrowsa Union shrinks to one branch inside the if
  • Generic[T] you writeone TypeVar ties a parameter to its return type
  • Variancecovariant (read-only) vs. contravariant (write-only) substitution

1.TypedDict

TypedDict declares the exact keys a dict should have and the type of each value — for when a dict is really "a record with a fixed shape," not an arbitrary mapping. The result is still a plain dict at runtime.

Key features

  • class Movie(TypedDict): title: str; year: int declares a dict shape with two required keys
  • A TypedDict value is a genuine, ordinary dict at runtime — isinstance(m, dict) is True
  • A type checker verifies the keys and value types match; Python itself does not
  • total=False marks all keys optional; NotRequired[T] (3.11+) marks one key optional
  • Unlike a dataclass: TypedDict has no __init__, no methods, no defaults — just a shape

Command

python
from typing import TypedDict

class Movie(TypedDict):
    title: str
    year: int

m: Movie = {"title": "Arrival", "year": 2016}

TypedDict declares a dict's expected keys and value types for a type checker. The result is a plain dict at runtime — nothing is validated when it runs.

2.Type narrowing and type guards

Type narrowing is a type checker shrinking int | str down to just str inside an if isinstance(x, str): block. A type guard is a function you write yourself, marked TypeGuard[T], that teaches the checker a custom narrowing rule.

Key features

  • isinstance(x, str) inside an if automatically narrows x to str for that branch
  • A type checker performs narrowing automatically for isinstance, type(), and None checks
  • A type guard is a function you define, returning TypeGuard[T], for custom narrowing logic
  • A TypeGuard function still returns a plain bool at runtime — narrowing is checker-only
  • Narrowing only lasts within the branch/scope where the check is known to hold
A Union narrows to one branch after the check

int | str

value could be either

isinstance(x, str)

the check

x is str here

narrowed inside the if

  1. int | str — value could be either
  2. isinstance(x, str) — the check
  3. x is str here — narrowed inside the if

Command

python
from typing import TypeGuard

def describe(value: int | str) -> str:
    if isinstance(value, str):
        return value.upper()   # narrowed to str
    return value + 1           # narrowed to int

def is_str_list(vals: list[object]) -> TypeGuard[list[str]]:
    return all(isinstance(v, str) for v in vals)

isinstance() inside an if narrows a Union automatically. TypeGuard[T] lets you write a custom narrowing function — its logic must actually match the claim.

3.Writing generic classes and functions

A generic function uses the same TypeVar for a parameter and its return type, so a type checker ties the two together. A generic class does the same across its methods by inheriting Generic[T].

Key features

  • A generic FUNCTION reuses one TypeVar between a parameter and the return type
  • first(items: list[T]) -> T: ties the list's element type directly to the return type
  • A generic CLASS inherits Generic[T] and uses T consistently across __init__ and methods
  • One generic definition replaces writing a near-identical function/class per concrete type
  • The concrete type is usually inferred by the type checker from what is actually passed in

Command

python
from typing import TypeVar, Generic

T = TypeVar("T")

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

class Stack(Generic[T]):                 # generic class
    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()

A generic function ties a parameter's TypeVar to its return type. A generic class inherits Generic[T] and reuses T across its methods.

4.Variance (conceptual)

Variance is whether Box[Dog] may be used where Box[Animal] is expected, given Dog is a subtype of Animal. Covariant allows it for read-only boxes; contravariant allows the reverse; invariant allows neither.

Key features

  • Covariant: Container[Dog] usable where Container[Animal] expected — read-only positions
  • Contravariant: opposite direction — write-only positions, like a callback argument
  • Invariant: no substitution either way — the default for a mutable container like list[T]
  • TypeVar("T_co", covariant=True) / TypeVar("T_contra", contravariant=True) declare variance
  • list[T] is invariant since it's readable AND writable — covariance risks the wrong subtype

Command

python
from typing import TypeVar, Generic

T_co = TypeVar("T_co", covariant=True)

class ReadOnlyBox(Generic[T_co]):
    def get(self) -> T_co: ...

Covariant = safe for read-only substitution. Contravariant = the reverse, for write-only. Invariant = no substitution — Python enforces none of this at runtime.

Remember

  • TypedDict is a plain dict at runtime — a type checker verifies its keys, Python never does.
  • isinstance(x, T) inside an if narrows x to T automatically for the rest of that branch.
  • A TypeGuard function is trusted completely — its logic must actually match what it claims to narrow.
  • A generic function only ties input to output if the SAME TypeVar appears on both sides.

Pro tip

  • Protocol and structural typing (the other two Advanced typing items) live in Object-Oriented Python — see seeAlso.
  • Reach for TypeGuard when isinstance() alone cannot express the check — e.g. verifying every element of a list.
  • Variance only matters for a checker; Python places zero runtime restriction on any substitution.
  • list[T] is invariant on purpose — a mutable, read+write container cannot safely be covariant.

Next up

  • Tools

Sources

  • typing — docs.python.org/3/library/typing.html
  • Every example executed on Python 3.14.3 · checked 2026-08-21

Spotted a mistake on this page? Tell us.