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"]
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"]

Why bother?

  • Documentation that can’t drift — the signature states the contract.
  • Editor support — autocomplete and inline errors.
  • Static checkingmypymypy flags 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.
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
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

Optional and Union

A value that may be NoneNone 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)
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)
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]Optional[X] is exactly X | NoneX | None. It does not mean the argument is optional — it means the value can be NoneNone.

Any, and when to avoid it

AnyAny 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))
any.py
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.

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
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

Type aliases

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]
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]

Generics with TypeVar

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
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[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
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

Other handy typing tools

ToolUse
Literal["a", "b"]Literal["a", "b"]Restrict to specific literal values.
FinalFinalMark a constant that shouldn’t be reassigned.
TypedDictTypedDictA dict with a fixed set of typed keys.
SequenceSequence / IterableIterable / MappingMappingAccept any sequence/iterable/mapping, not just listlist/dictdict.
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
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

Checking types with mypy

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"
terminal
$ 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 runtimedef f(x: int)def f(x: int) still accepts a string unless a checker complains.
  • Optional[X]Optional[X] is about NoneNone, not about omittable arguments (that’s a default value).
  • Use SequenceSequence/IterableIterable for 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+, or typingtyping equivalents on older versions.
  • Optional[X]Optional[X] / X | NoneX | None for nullable values; UnionUnion / A | BA | B for multiple types.
  • Type functions with CallableCallable, reuse complex types with aliases, and write reusable code with TypeVarTypeVar/GenericGeneric.
  • Prefer IterableIterable/SequenceSequence for inputs; run mypymypy to enforce.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did