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

Richer 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.12 type statement 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

Example · python
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.

Example · python
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.

Example · python
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([]))            # None

Callable and Union types

Annotates a higher-order function with Callable and a nullable return with Union[int, None].

Example · python
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

  • Be the first to comment on this lesson.