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"]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"]Why bother?
- Documentation that can’t drift — the signature states the contract.
- Editor support — autocomplete and inline errors.
- Static checking —
mypymypyflags type mismatches without running the code. - Safer refactors — change a type, and the checker shows every break.
Built-in collection generics
Since Python 3.9 you can subscript the built-in containers directly. (Older code imports ListList, DictDict, etc. from typingtyping.)
| Modern (3.9+) | Legacy (from typing import ...from typing import ...) | Means |
|---|---|---|
list[int]list[int] | List[int]List[int] | A list of ints. |
dict[str, int]dict[str, int] | Dict[str, int]Dict[str, int] | A dict: str keys, int values. |
tuple[int, str]tuple[int, str] | Tuple[int, str]Tuple[int, str] | A 2-tuple. |
set[str]set[str] | Set[str]Set[str] | A set of strings. |
tuple[int, ...]tuple[int, ...] | 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 resultdef 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
A value that may be NoneNone 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)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)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]Optional[X]is exactlyX | NoneX | None. It does not mean the argument is optional — it means the value can beNoneNone.
Any, and when to avoid it
AnyAny 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))from typing import Any
def debug(value: Any) -> None: # accepts literally anything
print(repr(value))Callable — typing functions
Callable[[ArgTypes], ReturnType]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)) # 7from 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
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]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
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 strfrom 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]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()) # 42from 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
| Tool | Use |
|---|---|
Literal["a", "b"]Literal["a", "b"] | Restrict to specific literal values. |
FinalFinal | Mark a constant that shouldn’t be reassigned. |
TypedDictTypedDict | A dict with a fixed set of typed keys. |
SequenceSequence / IterableIterable / MappingMapping | Accept any sequence/iterable/mapping, not just listlist/dictdict. |
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))) # 9from 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
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"$ pip install mypy
$ mypy myscript.py
myscript.py:3: error: Argument 1 to "greet" has incompatible type "int"; expected "str"Common pitfalls
- Hints don’t enforce anything at runtime —
def f(x: int)def f(x: int)still accepts a string unless a checker complains. Optional[X]Optional[X]is aboutNoneNone, not about omittable arguments (that’s a default value).- Use
SequenceSequence/IterableIterablefor parameters, concrete types for return values — accept broadly, return precisely. - Don’t overuse
AnyAny— it silences the checker.
Practice Exercises
Exercise 1 – Annotate a function
Exercise 2 – Optional return
Exercise 3 – A generic ‘first’ function
Summary
- Type hints document expected types; they’re ignored at runtime but power editors and
mypymypy. - Use built-in generics (
list[int]list[int],dict[str, int]dict[str, int]) on 3.9+, ortypingtypingequivalents on older versions. Optional[X]Optional[X]/X | NoneX | Nonefor nullable values;UnionUnion/A | BA | Bfor multiple types.- Type functions with
CallableCallable, reuse complex types with aliases, and write reusable code withTypeVarTypeVar/GenericGeneric. - Prefer
IterableIterable/SequenceSequencefor inputs; runmypymypyto enforce.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
