Type Hints and Static Typing¶
Type hints are optional annotations for documentation, IDE support, and static analysis (mypy). They do NOT enforce types at runtime. Starting with function signatures provides the most value for least effort.
Key Facts¶
- Python 3.9+: use built-in
list[str],dict[str, int]directly (notyping.List) - Python 3.10+: use
int | strinstead ofUnion[int, str] Optional[X]is shorthand forX | NoneProtocolenables structural subtyping (duck typing with type checking)TypeVarcreates generic type variables- mypy is the standard static type checker; run
mypy script.py
Patterns¶
Basic Annotations¶
# Variables (Python 3.6+)
name: str = "Alice"
age: int = 30
is_active: bool = True
# Functions (Python 3.5+)
def greet(name: str, times: int = 1) -> str:
return f"Hello, {name}! " * times
Container Types¶
# Python 3.9+
names: list[str] = ["Alice", "Bob"]
scores: dict[str, int] = {"Alice": 95}
coords: tuple[float, float] = (1.0, 2.0)
unique: set[int] = {1, 2, 3}
# Python 3.5-3.8 (import from typing)
from typing import List, Dict, Tuple, Set
Optional and Union¶
def find_user(user_id: int) -> str | None: # Python 3.10+
...
# Pre-3.10
from typing import Optional, Union
def find_user(user_id: int) -> Optional[str]:
...
def process(value: Union[int, str]) -> str:
...
Callable¶
from typing import Callable
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
callback: Callable[..., None] # any args, returns None
Generics and TypeVar¶
from typing import TypeVar, Generic
T = TypeVar('T')
def first(items: list[T]) -> T:
return items[0]
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
stack: Stack[int] = Stack()
Protocol (Structural Subtyping)¶
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("Drawing circle")
def render(shape: Drawable) -> None:
shape.draw()
render(Circle()) # OK - Circle has draw() method, no inheritance needed
Special Types¶
from typing import Literal, Final, TypedDict, NewType
# Restrict to specific values
def set_mode(mode: Literal["read", "write"]) -> None: ...
# Constants
MAX_SIZE: Final = 100
# Typed dictionaries
class UserDict(TypedDict):
name: str
age: int
# Distinct types
UserId = NewType('UserId', int)
Type Aliases¶
Flexible Parameter Types¶
from typing import Sequence, Mapping, Iterable
# Use abstract types for parameters (more flexible)
def process(items: Sequence[int]) -> list[int]: # accepts list, tuple, str
return sorted(items)
# Use concrete types for return values (caller knows exactly what they get)
mypy Configuration¶
# mypy.ini or pyproject.toml [tool.mypy]
[mypy]
python_version = 3.10
strict = true
warn_return_any = true
disallow_untyped_defs = true
Gotchas¶
- Type hints are NOT enforced at runtime - they are for tools only
def f(x: str = None)should bedef f(x: str | None = None)-Noneis notstr- Avoid
Any- it disables type checking for that value __annotations__dict stores raw annotations; usetyping.get_type_hints()for resolved types- Circular imports with type hints: use
from __future__ import annotationsorTYPE_CHECKING
See Also¶
- oop advanced - Generic classes, Protocol for structural subtyping
- fastapi pydantic validation - Pydantic uses type hints for runtime validation
- testing with pytest - mypy in CI