Type Hints & Generics
Annotate code for clarity and tooling — from simple hints to Generic, TypedDict and the type statement.
Type hints describe the types your code expects. Python does not enforce them at runtime — they are for humans and tools: editors autocomplete, mypy catches mismatches before you run, and readers understand your intent at a glance.
The modern, import-light style
Since Python 3.10 you write unions with | and use built-in generics directly — no typing.List or typing.Optional needed:
def greet(names: list[str], greeting: str | None = None) -> str:
greeting = greeting or 'Hello'
return ', '.join(f'{greeting}, {n}' for n in names)
print(greet(['Ada', 'Grace'])) # Hello, Ada, Hello, GraceRicher building blocks
TypedDict— a dict with a known set of typed keys, great for JSON-shaped data.- Generic functions/classes —
def first[T](xs: list[T]) -> T(3.12+ syntax). type Alias = ...— the 3.12typestatement names a complex hint.Callable[[int], str]— describe a function you accept as a parameter.
Hints are optional and incremental: add them where they clarify — public function signatures, data shapes, tricky returns — and skip them where the code is already obvious.
Example
from typing import TypedDict
from collections.abc import Callable
# A dict with a known, typed shape
class Product(TypedDict):
name: str
price: float
tags: list[str]
# 3.12 type-alias statement names a gnarly type once
type Predicate = Callable[[Product], bool]
# A generic helper (3.12 inline syntax), plus a Callable parameter
def filter_items[T](items: list[T], keep: Callable[[T], bool]) -> list[T]:
return [x for x in items if keep(x)]
catalog: list[Product] = [
{'name': 'Pen', 'price': 1.5, 'tags': ['office']},
{'name': 'Laptop', 'price': 999.0, 'tags': ['tech', 'sale']},
{'name': 'Mug', 'price': 8.0, 'tags': ['sale']},
]
on_sale: Predicate = lambda p: 'sale' in p['tags']
for p in filter_items(catalog, on_sale):
print(f"{p['name']:<8} ${p['price']:.2f}")
cheap = filter_items(catalog, lambda p: p['price'] < 10)
print('cheap:', [p['name'] for p in cheap])
# Output:
# Laptop $999.00
# Mug $8.00
# cheap: ['Pen', 'Mug']When to use it
- A public library annotates all function signatures so IDEs provide accurate autocomplete for users.
- A mypy check in CI catches a function that accidentally returns None where the annotation says str.
- A TypedDict describes the shape of a JSON API response so mypy can validate field access throughout the codebase.
More examples
TypedDict for structured dicts
Defines a TypedDict to type-check dict accesses, giving mypy visibility into dict key types.
from typing import TypedDict
class UserRecord(TypedDict):
name: str
age: int
email: str
def display(user: UserRecord) -> str:
return f"{user['name']} ({user['age']})"
record: UserRecord = {'name': 'Alice', 'age': 30, 'email': '[email protected]'}
print(display(record)) # Alice (30)Generic function with TypeVar
Uses TypeVar so the return type matches the list element type, preserving type information for callers.
from typing import TypeVar, List
T = TypeVar('T')
def first(items: List[T]) -> T | None:
return items[0] if items else None
print(first([1, 2, 3])) # 1
print(first(['a', 'b'])) # a
print(first([])) # NoneCallable and Union types
Annotates a higher-order function with Callable and a nullable return with Union[int, None].
from typing import Callable, Union
def apply(fn: Callable[[int], int], value: int) -> int:
return fn(value)
def maybe_square(x: Union[int, None]) -> Union[int, None]:
return x ** 2 if x is not None else None
print(apply(lambda n: n * 2, 5)) # 10
print(maybe_square(None)) # None
print(maybe_square(4)) # 16
Discussion