Skip to content

Python Type Hints & the typing Module

Type hints annotate the expected types of variables, parameters, and return values. They don’t change how your code runs — Python ignores them at runtime — but they make code self-documenting and let tools like mypy catch bugs before you run anything. This page goes well beyond basic annotations (see also the Function Annotations page for the basics).

quickstart.py
def greet(name: str, excited: bool = False) -> str:
    msg = f"Hello, {name}"
    return msg + "!" if excited else msg
 
age: int = 30
pi: float = 3.14159
names: list[str] = ["Ada", "Bob"]
diagram annotations are data; the typing tools are how you read them mermaid
At runtime an annotation may be an object or a plain string, depending on how it was written and which Python version is running. get_type_hints resolves whatever is there into real objects, and get_origin and get_args take those apart. None of this checks anything -- isinstance flatly refuses a parameterised generic.
  • Documentation that can’t drift — the signature states the contract.
  • Editor support — autocomplete and inline errors.
  • Static checkingmypy flags type mismatches without running the code.
  • Safer refactors — change a type, and the checker shows every break.

Since Python 3.9 you can subscript the built-in containers directly. (Older code imports List, Dict, etc. from typing.)

Modern (3.9+)Legacy (from typing import ...)Means
list[int]List[int]A list of ints.
dict[str, int]Dict[str, int]A dict: str keys, int values.
tuple[int, str]Tuple[int, str]A 2-tuple.
set[str]Set[str]A set of strings.
tuple[int, ...]Tuple[int, ...]A tuple of any length of ints.
collections.py
def total(prices: list[float]) -> float:
    return sum(prices)
 
def counts(words: list[str]) -> dict[str, int]:
    result: dict[str, int] = {}
    for w in words:
        result[w] = result.get(w, 0) + 1
    return result

A value that may be None is Optional. A value that may be one of several types is a Union.

optional_union.py
from typing import Optional, Union
 
# Optional[str] means "str or None"
def find_user(uid: int) -> Optional[str]:
    users = {1: "Ada"}
    return users.get(uid)        # returns str or None
 
# Union means "any of these types"
def to_int(x: Union[int, str]) -> int:
    return int(x)

Since Python 3.10 you can use the cleaner | syntax:

pipe_union.py
def find_user(uid: int) -> str | None:      # same as Optional[str]
    ...
 
def to_int(x: int | str) -> int:            # same as Union[int, str]
    return int(x)

Optional[X] is exactly X | None. It does not mean the argument is optional — it means the value can be None.

Any disables type checking for that value — use it sparingly, as it defeats the purpose.

any.py
from typing import Any
 
def debug(value: Any) -> None:    # accepts literally anything
    print(repr(value))

Callable[[ArgTypes], ReturnType] describes a function passed as a value.

callable.py
from typing import Callable
 
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
    return func(a, b)
 
print(apply(lambda x, y: x + y, 3, 4))   # 7

Give a complex type a readable name.

aliases.py
from typing import Union
 
# A reusable alias
Number = Union[int, float]
Matrix = list[list[float]]
 
def scale(m: Matrix, factor: Number) -> Matrix:
    return [[cell * factor for cell in row] for row in m]

A TypeVar lets you write functions and classes that work for any type while preserving the relationship between input and output.

generics.py
from typing import TypeVar
 
T = TypeVar("T")
 
def first(items: list[T]) -> T:      # returns the SAME type the list holds
    return items[0]
 
x: int = first([1, 2, 3])            # checker knows x is int
y: str = first(["a", "b"])           # checker knows y is str

Generic classes subclass Generic[T]:

generic_class.py
from typing import Generic, TypeVar
 
T = TypeVar("T")
 
class Box(Generic[T]):
    def __init__(self, item: T) -> None:
        self.item = item
    def get(self) -> T:
        return self.item
 
b: Box[int] = Box(42)
print(b.get())   # 42
ToolUse
Literal["a", "b"]Restrict to specific literal values.
FinalMark a constant that shouldn’t be reassigned.
TypedDictA dict with a fixed set of typed keys.
Sequence / Iterable / MappingAccept any sequence/iterable/mapping, not just list/dict.
abc_types.py
from typing import Iterable
 
# Accept ANY iterable of ints (list, tuple, generator, ...)
def total(nums: Iterable[int]) -> int:
    return sum(nums)
 
print(total([1, 2, 3]))      # 6
print(total((4, 5)))         # 9

Hints are not enforced at runtime — run a checker to catch mistakes.

terminal
$ pip install mypy
$ mypy myscript.py
myscript.py:3: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
  • Hints don’t enforce anything at runtimedef f(x: int) still accepts a string unless a checker complains.
  • Optional[X] is about None, not about omittable arguments (that’s a default value).
  • Use Sequence/Iterable for parameters, concrete types for return values — accept broadly, return precisely.
  • Don’t overuse Any — it silences the checker.

Exercise 3 – A generic ‘first’ function

Section titled “Exercise 3 – A generic ‘first’ function”
sketch An annotation is stored, never checked p5.js
The interpreter records the annotation on the function object and then ignores it completely. Passing the wrong type produces whatever the body happens to do -- here, string concatenation from a function annotated to return an int. The value of annotations comes entirely from tools that read them before or around the run.
pch.quizTag pch.quizDefaultTitle
  1. `def add(a: int, b: int) -> int`. What does `add('x', 'y')` do?

    pch.quizShowAnswer

    B — Returns `'xy'` with no error — Verified. The interpreter never consults annotations, so the body simply concatenated the strings — returning a `str` from a function annotated `-> int`.

  2. Where does the value of type annotations actually come from?

    pch.quizShowAnswer

    B — Tools that read them: type checkers, editors, and libraries like pydantic — `mypy` would reject that call before you ran it. The failure annotations prevent is caught ahead of execution, not during it.

  3. In Python 3.14, when is an annotation evaluated?

    pch.quizShowAnswer

    B — Lazily — only when `__annotations__` is read — PEP 649 landed in 3.14. `def f(x: NotDefinedYet)` is accepted at definition and raises `NameError` only when the annotations are read. On 3.11 it raised at definition time.

  4. What does `get_type_hints(f)` do that reading `f.__annotations__` may not?

    pch.quizShowAnswer

    B — Resolves string annotations into real objects — Annotations may be stored as strings. `get_type_hints` resolves them, and `get_origin`/`get_args` then take apart a generic like `list[int]`.

  • Type hints document expected types; they’re ignored at runtime but power editors and mypy.
  • Use built-in generics (list[int], dict[str, int]) on 3.9+, or typing equivalents on older versions.
  • Optional[X] / X | None for nullable values; Union / A | B for multiple types.
  • Type functions with Callable, reuse complex types with aliases, and write reusable code with TypeVar/Generic.
  • Prefer Iterable/Sequence for inputs; run mypy to enforce.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading