Skip to main content
Advertisement

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

TypePurposeExample
Optional[X]Can be NoneOptional[str]str | None
Union[X, Y]Multiple types allowedUnion[int, str]int | str
Literal[v]Only specific valuesLiteral["GET", "POST"]
TypeVarGeneric type variableT = TypeVar("T")
Generic[T]Generic classclass Stack(Generic[T])
Callable[[X], Y]Function typeCallable[[int], str]
ClassVar[T]Class variable distinctionClassVar[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.

Advertisement