Type Hints & Static Analysis
Write safer Python with type hints, the typing module, generic types, Protocol, and static analysis tools like mypy and pyright.
Python is dynamically typed, but since Python 3.5 you can annotate variables, function parameters, and return values with type hints. Combined with static analysis tools, this catches bugs before runtime.
Basic Type Annotations
def greet(name: str) -> str:
return f"Hello, {name}!"
age: int = 25
scores: list[float] = [95.5, 87.0, 91.3]
Type hints are optional at runtime — Python ignores them during execution. They serve documentation and static analysis tools.
Common Built-in Types
from typing import Optional, Union
def find_user(user_id: int) -> Optional[str]:
"""Returns username or None if not found."""
...
def parse(value: Union[int, str]) -> int:
"""Accepts int or str, always returns int."""
return int(value)
| Annotation | Meaning |
|---|---|
str, int, float, bool |
Primitive types |
list[str] |
List of strings (Python 3.9+) |
dict[str, int] |
Dict with string keys, int values |
tuple[int, str] |
Fixed-length tuple |
Optional[T] |
T or None |
Union[A, B] |
Either type A or B |
Generic Collections
For Python 3.8 and earlier, import from typing:
from typing import List, Dict, Tuple, Set
def average(numbers: List[float]) -> float:
return sum(numbers) / len(numbers)
cache: Dict[str, int] = {}
Python 3.9+ allows built-in generics directly: list[float], dict[str, int].
TypedDict and NamedTuple
from typing import TypedDict
class User(TypedDict):
name: str
age: int
email: str
def create_user(data: User) -> User:
return data
user: User = {"name": "Alice", "age": 30, "email": "[email protected]"}
Protocol (Structural Subtyping)
Protocol defines an interface without inheritance:
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
def render(shape: Drawable) -> None:
shape.draw()
class Circle:
def draw(self) -> None:
print("Drawing circle")
render(Circle()) # Circle satisfies Drawable structurally
Callable Types
from typing import Callable
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
apply(lambda x, y: x + y, 3, 5) # → 8
Static Analysis with mypy
Install and run mypy to check types across your codebase:
pip install mypy
mypy your_module.py
Example — mypy catches this error before runtime:
def add(a: int, b: int) -> int:
return a + b
add("hello", 5) # mypy error: Argument 1 has incompatible type "str"
Configuration
Create mypy.ini or add to pyproject.toml:
[tool.mypy]
python_version = "3.11"
strict = true
ignore_missing_imports = true
When to Use Type Hints
- Public APIs and libraries — always annotate function signatures.
- Large codebases — types pay off as teams and code grow.
- Data processing pipelines — catch shape mismatches early.
- Skip for quick scripts, notebooks, and throwaway prototypes.
Type hints don’t replace tests, but they complement them by catching an entire class of errors at development time.
Modern typing Features (Python 3.10+)
Union with |
def parse_id(value: int | str) -> int:
return int(value)
def find_user(user_id: int) -> str | None:
...
Prefer X | Y over Union[X, Y] in new code.
TypeAlias and Literal
from typing import Literal, TypeAlias
Status = Literal["pending", "active", "archived"]
UserId: TypeAlias = int | str
def set_status(status: Status) -> None:
...
set_status("active") # OK
set_status("unknown") # mypy error
Self — Fluent Interfaces
from typing import Self
class Builder:
def reset(self) -> Self:
return self
def add(self, item: str) -> Self:
self.items.append(item)
return self
Typed Classes with dataclasses
Combine @dataclass with type hints for clear data models:
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
in_stock: bool = True
def apply_discount(product: Product, pct: float) -> Product:
return Product(product.name, product.price * (1 - pct), product.in_stock)
See Advanced Topics for dataclass details.
Runtime Type Checking with @beartype
Optional runtime enforcement (mypy is static only):
pip install beartype
from beartype import beartype
@beartype
def divide(a: int, b: int) -> float:
return a / b
divide(10, 2) # OK
divide("10", 2) # BeartypeError at runtime
Use sparingly — adds overhead. Prefer mypy in CI for most projects.
pyright and IDE Integration
VS Code with Pylance uses pyright for inline type errors:
// .vscode/settings.json
{
"python.analysis.typeCheckingMode": "basic"
}
Levels: off → basic → strict. Start with basic and increase as coverage improves.
Gradual Typing Strategy
For existing codebases, don’t annotate everything at once:
- Enable mypy on new files only (
# type: ignoreon legacy modules temporarily) - Annotate public function signatures first
- Add
strict = truemodule by module - Run mypy in CI — block merges on type errors
mypy src/new_module.py --strict
mypy src/ # expand scope over time
Related
- FastAPI — Pydantic models extend type hints for API validation
- Advanced Testing — run mypy alongside pytest in CI