Advanced Type Hints
Python's type hint system can express complex type relationships via the typing module. Using generics, TypeVar, and Protocol, you can write reusable, type-safe code.
Optional and Union
from typing import Optional, Union
# Optional[X] is shorthand for Union[X, None]
def find_user(user_id: int) -> Optional[str]:
users = {1: "Alice", 2: "Bob"}
return users.get(user_id) # Returns None if not found
result = find_user(1)
if result is not None:
print(result.upper()) # Type narrowing
# Union: one of several types
def process(value: Union[int, str, list]) -> str:
if isinstance(value, int):
return f"Integer: {value}"
elif isinstance(value, str):
return f"String: {value}"
else:
return f"List: {len(value)} items"
print(process(42))
print(process("hello"))
print(process([1, 2, 3]))
# Python 3.10+: X | Y syntax (shorthand for Union)
def greet(name: str | None = None) -> str:
return f"Hello, {name}!" if name else "Hello, World!"
print(greet())
print(greet("Alice"))
Literal — Allow Only Specific Values
from typing import Literal
# Literal: only specific constant values allowed
def set_direction(direction: Literal["north", "south", "east", "west"]) -> None:
print(f"Direction set: {direction}")
set_direction("north") # OK
# set_direction("up") # Type checker error
# Literal for status codes
StatusCode = Literal[200, 201, 400, 401, 403, 404, 500]
def create_response(status: StatusCode, body: str) -> dict:
return {"status": status, "body": body}
# Multiple Literals combined
LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
def log(level: LogLevel, message: str) -> None:
print(f"[{level}] {message}")
log("INFO", "Server started")
log("ERROR", "Database connection failed")
TypeVar — Generic Type Variables
from typing import TypeVar, Sequence
T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")
# Function that preserves the type
def first(items: Sequence[T]) -> T | None:
return items[0] if items else None
# Type inference: first([1, 2, 3]) returns int
nums: list[int] = [1, 2, 3]
strs: list[str] = ["a", "b", "c"]
n = first(nums) # n's type: int | None
s = first(strs) # s's type: str | None
# bound: restrict to an upper type
from typing import TypeVar
N = TypeVar("N", bound=int | float)
def double(x: N) -> N:
return x * 2 # type: ignore
print(double(5)) # 10
print(double(3.14)) # 6.28
# constraints: specify allowed types explicitly
AnyStr = TypeVar("AnyStr", str, bytes)
def to_upper(text: AnyStr) -> AnyStr:
return text.upper()
print(to_upper("hello")) # HELLO
print(to_upper(b"hello")) # b'HELLO'
Generic Classes
from typing import Generic, TypeVar
T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")
class Stack(Generic[T]):
"""Type-safe stack"""
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
if not self._items:
raise IndexError("Empty stack")
return self._items.pop()
def peek(self) -> T:
if not self._items:
raise IndexError("Empty stack")
return self._items[-1]
def __len__(self) -> int:
return len(self._items)
def __repr__(self) -> str:
return f"Stack({self._items})"
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
print(int_stack.pop()) # 2
print(int_stack)
str_stack: Stack[str] = Stack()
str_stack.push("hello")
str_stack.push("world")
print(str_stack.peek())
# Generic with two type parameters
class Pair(Generic[K, V]):
def __init__(self, key: K, value: V) -> None:
self.key = key
self.value = value
def swap(self) -> "Pair[V, K]":
return Pair(self.value, self.key)
def __repr__(self) -> str:
return f"Pair({self.key!r}, {self.value!r})"
p = Pair("name", "Alice")
swapped = p.swap()
print(p) # Pair('name', 'Alice')
print(swapped) # Pair('Alice', 'name')
Callable Type Hints
from typing import Callable
# Callable[[ArgType, ...], ReturnType]
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
print(apply(lambda x, y: x + y, 3, 5)) # 8
print(apply(lambda x, y: x * y, 3, 5)) # 15
# Higher-order function type hints
def make_multiplier(factor: int) -> Callable[[int], int]:
def multiplier(x: int) -> int:
return x * factor
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# Variable-argument Callable
from typing import Any
Handler = Callable[..., None] # Any arguments, returns None
def register_handler(event: str, handler: Handler) -> None:
print(f"Handler registered: {event}")
handler()
register_handler("click", lambda: print("Clicked!"))
Type and ClassVar
from typing import ClassVar, Type
class Animal:
species_count: ClassVar[int] = 0 # Class variable, not instance variable
def __init__(self, name: str) -> None:
self.name = name
Animal.species_count += 1
class Dog(Animal):
tricks: ClassVar[list[str]] = []
def learn_trick(self, trick: str) -> None:
Dog.tricks.append(trick)
# Type[T]: the class itself as a type hint
def create_animal(cls: Type[Animal], name: str) -> Animal:
return cls(name)
dog = create_animal(Dog, "Rex")
print(type(dog)) # <class 'Dog'>
print(Animal.species_count) # 1
Real-World Example: Type-Safe Event System
from typing import Callable, Generic, TypeVar, Any
from dataclasses import dataclass, field
EventData = TypeVar("EventData")
@dataclass
class Event(Generic[EventData]):
name: str
data: EventData
Handler = Callable[[Event[Any]], None]
class EventBus:
def __init__(self) -> None:
self._handlers: dict[str, list[Handler]] = {}
def subscribe(self, event_name: str, handler: Handler) -> None:
if event_name not in self._handlers:
self._handlers[event_name] = []
self._handlers[event_name].append(handler)
def publish(self, event: Event[Any]) -> None:
for handler in self._handlers.get(event.name, []):
handler(event)
# Usage
bus = EventBus()
def on_user_created(event: Event[dict]) -> None:
print(f"New user created: {event.data['name']}")
def on_user_created_email(event: Event[dict]) -> None:
print(f"Welcome email sent to: {event.data['email']}")
bus.subscribe("user_created", on_user_created)
bus.subscribe("user_created", on_user_created_email)
bus.publish(Event("user_created", {"name": "Alice", "email": "alice@example.com"}))
Summary
| Type | Purpose | Example |
|---|---|---|
Optional[X] | Can be None | Optional[str] → str | None |
Union[X, Y] | Multiple types allowed | Union[int, str] → int | str |
Literal[v] | Only specific values | Literal["GET", "POST"] |
TypeVar | Generic type variable | T = TypeVar("T") |
Generic[T] | Generic class | class Stack(Generic[T]) |
Callable[[X], Y] | Function type | Callable[[int], str] |
ClassVar[T] | Class variable distinction | ClassVar[int] |
Since Python 3.9+, built-in types like list[int] and dict[str, int] can be used directly as generics, reducing the need to import from typing.