typing
The typing module provides runtime support for type hints introduced in PEP 484 and subsequent PEPs. It enables developers to annotate functions, variables, and classes with expected types, which static analysis tools like mypy can use to catch type errors before runtime.
Syntax
from typing import TypeVar, Generic, Protocol, Optional, Union, List, Dict
Key Type Constructs
TypeVar
TypeVar creates a generic type variable that can be any one type within a bound:
from typing import TypeVar
T = TypeVar('T')
def first_element(lst: list[T]) -> T | None:
return lst[0] if lst else None
print(first_element([1, 2, 3]))
# 1
print(first_element(['a', 'b']))
# a
Generic
Generic is the base class for generic types:
from typing import TypeVar, Generic
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self):
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
int_stack: Stack[int] = Stack()
int_stack.push(10)
int_stack.push(20)
print(int_stack.pop())
# 20
Protocol
Protocol defines structural subtyping interfaces without inheritance:
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("Drawing circle")
class Square:
def draw(self) -> None:
print("Drawing square")
def render(d: Drawable) -> None:
d.draw()
render(Circle())
# Drawing circle
render(Square())
# Drawing square
Optional and Union
Optional and Union describe types that can be one of multiple options:
from typing import Optional, Union
def greet(name: Optional[str]) -> str:
if name is None:
return "Hello, stranger!"
return f"Hello, {name}!"
print(greet(None))
# Hello, stranger!
print(greet("Alice"))
# Hello, Alice!
def process(value: Union[int, str]) -> str:
return str(value)
print(process(42))
# 42
print(process("hello"))
# hello
Common Type Aliases
List, Dict, Tuple
These generic types specify container contents:
from typing import List, Dict, Tuple, Set
names: List[str] = ["Alice", "Bob", "Charlie"]
ages: Dict[str, int] = {"Alice": 30, "Bob": 25}
coordinates: Tuple[int, int] = (10, 20)
unique_ids: Set[int] = {1, 2, 3}
print(names)
# ['Alice', 'Bob', 'Charlie']
print(ages)
# {'Alice': 30, 'Bob': 25}
Callable
Callable represents callable objects (functions):
from typing import Callable
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
def multiply(x: int, y: int) -> int:
return x * y
result = apply(multiply, 3, 4)
print(result)
# 12
Literal
Literal restricts a value to specific literal values:
from typing import Literal
def set_status(status: Literal["pending", "approved", "rejected"]) -> None:
print(f"Status set to: {status}")
set_status("pending")
# Status set to: pending
set_status("approved")
# Status set to: approved
Type Guards and Cast
TypeGuard
TypeGuard narrows types within conditional blocks:
from typing import TypeGuard
def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in val)
def process(items: list[object]) -> None:
if is_string_list(items):
print(" ".join(items))
else:
print("Not all strings")
process(["a", "b", "c"])
# a b c
process([1, 2, "x"])
# Not all strings
cast
cast tells the type checker to treat a value as a different type:
from typing import cast
def unsafe_parse(data: dict[str, object]) -> list[int]:
items = data["items"]
return cast(list[int], items)
result = unsafe_parse({"items": [1, 2, 3]})
print(result)
# [1, 2, 3]
Common Patterns
Type Checking Functions
Use isinstance() and type guards to help type checkers:
from typing import TypeGuard
def is_dict(val: object) -> TypeGuard[dict]:
return isinstance(val, dict)
def process(val: object) -> None:
if is_dict(val):
print(val.keys())
Overload Functions
Use @overload for multiple function signatures:
from typing import overload, Union
@overload
def process(value: int) -> int: ...
@overload
def process(value: str) -> str: ...
@overload
def process(value: list[int]) -> list[int]: ...
def process(value: Union[int, str, list[int]]) -> Union[int, str, list[int]]:
if isinstance(value, list):
return [x * 2 for x in value]
if isinstance(value, str):
return value.upper()
return value * 2
print(process(5))
# 10
print(process("hello"))
# HELLO
print(process([1, 2, 3]))
# [2, 4, 6]
Final and Read-Only
Use Final for constants and ReadOnly for immutable typed dict keys:
from typing import Final, TypedDict, ReadOnly
MAX_SIZE: Final = 1000
class Config(TypedDict):
name: str
value: ReadOnly[int]
config: Config = {"name": "app", "value": 42}