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).
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"]flowchart TD A["def f(a: 'list[int]', b: Optional[str]) -> Union[int, str]"] --> B["f.__annotations__"] B --> C["may contain STRINGS as written"] C --> D["get_type_hints(f)"] D --> E["resolved objects: list[int], str | None, int | str"] E --> F["get_origin(list[int]) -> list"] E --> G["get_args(list[int]) -> (int,)"] H["isinstance(5, list[int])"] --> I["TypeError: cannot be a parameterized generic"] E --> J["Optional[str] IS Union[str, None] -- the same object"]
Why bother?
Section titled “Why bother?”- Documentation that can’t drift — the signature states the contract.
- Editor support — autocomplete and inline errors.
- Static checking —
mypyflags type mismatches without running the code. - Safer refactors — change a type, and the checker shows every break.
Built-in collection generics
Section titled “Built-in collection generics”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. |
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 resultOptional and Union
Section titled “Optional and Union”A value that may be None is Optional. A value that may be one of several types is a Union.
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:
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 exactlyX | None. It does not mean the argument is optional — it means the value can beNone.
Any, and when to avoid it
Section titled “Any, and when to avoid it”Any disables type checking for that value — use it sparingly, as it defeats the purpose.
from typing import Any
def debug(value: Any) -> None: # accepts literally anything
print(repr(value))Callable — typing functions
Section titled “Callable — typing functions”Callable[[ArgTypes], ReturnType] describes a function passed as a value.
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)) # 7Type aliases
Section titled “Type aliases”Give a complex type a readable name.
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]Generics with TypeVar
Section titled “Generics with TypeVar”A TypeVar lets you write functions and classes that work for any type while preserving the relationship between input and output.
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 strGeneric classes subclass Generic[T]:
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()) # 42Other handy typing tools
Section titled “Other handy typing tools”| Tool | Use |
|---|---|
Literal["a", "b"] | Restrict to specific literal values. |
Final | Mark a constant that shouldn’t be reassigned. |
TypedDict | A dict with a fixed set of typed keys. |
Sequence / Iterable / Mapping | Accept any sequence/iterable/mapping, not just list/dict. |
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))) # 9Checking types with mypy
Section titled “Checking types with mypy”Hints are not enforced at runtime — run a checker to catch mistakes.
$ pip install mypy
$ mypy myscript.py
myscript.py:3: error: Argument 1 to "greet" has incompatible type "int"; expected "str"Common pitfalls
Section titled “Common pitfalls”- Hints don’t enforce anything at runtime —
def f(x: int)still accepts a string unless a checker complains. Optional[X]is aboutNone, not about omittable arguments (that’s a default value).- Use
Sequence/Iterablefor parameters, concrete types for return values — accept broadly, return precisely. - Don’t overuse
Any— it silences the checker.
Practice Exercises
Section titled “Practice Exercises”Exercise 1 – Annotate a function
Section titled “Exercise 1 – Annotate a function”Exercise 2 – Optional return
Section titled “Exercise 2 – Optional return”Exercise 3 – A generic ‘first’ function
Section titled “Exercise 3 – A generic ‘first’ function”Check yourself
Section titled “Check yourself”-
`def add(a: int, b: int) -> int`. What does `add('x', 'y')` do?
Verified. The interpreter never consults annotations, so the body simply concatenated the strings — returning a `str` from a function annotated `-> int`.
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`.
-
Where does the value of type annotations actually come from?
`mypy` would reject that call before you ran it. The failure annotations prevent is caught ahead of execution, not during it.
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.
-
In Python 3.14, when is an annotation evaluated?
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.
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.
-
What does `get_type_hints(f)` do that reading `f.__annotations__` may not?
Annotations may be stored as strings. `get_type_hints` resolves them, and `get_origin`/`get_args` then take apart a generic like `list[int]`.
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]`.
Summary
Section titled “Summary”- 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+, ortypingequivalents on older versions. Optional[X]/X | Nonefor nullable values;Union/A | Bfor multiple types.- Type functions with
Callable, reuse complex types with aliases, and write reusable code withTypeVar/Generic. - Prefer
Iterable/Sequencefor inputs; runmypyto enforce.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading