python-mypy
Python mypy — Static Type Checking
Mypy is an optional static type checker for Python that catches type errors at development time without runtime overhead. It combines the expressive power of Python with a powerful type system based on PEP 484, enabling gradual migration from dynamic to static typing.
Installation
pip install mypy
# Install type stubs for common third-party libraries
pip install types-requests types-PyYAML types-boto3
# Run mypy
mypy src/
mypy --strict src/
Core Annotation Patterns
Functions
Annotate all function signatures — return type and all parameters:
from collections.abc import Iterable, Sequence
def process(items: list[str], limit: int = 10) -> list[str]:
return items[:limit]
# Use abstract types for parameters (accept more, restrict less)
def summarize(records: Iterable[dict[str, str]]) -> str:
return ", ".join(r["name"] for r in records)
# None return type for procedures
def log_event(event: str, level: str = "INFO") -> None:
print(f"[{level}] {event}")
Optional and Union
# Python 3.10+ syntax (preferred)
def find_user(user_id: int) -> str | None:
...
# Python 3.9 and earlier
from typing import Optional, Union
def find_user(user_id: int) -> Optional[str]:
...
# Union for multiple possible types
def normalize_id(user_id: int | str) -> str:
if isinstance(user_id, int):
return f"user-{user_id}"
return user_id
Classes
from typing import ClassVar
class Repository:
default_limit: ClassVar[int] = 100
def __init__(self, name: str, url: str) -> None:
self.name = name
self.url = url
self._cache: dict[str, list[str]] = {}
def fetch(self, query: str) -> list[str]:
return self._cache.get(query, [])
TypedDict for Structured Dicts
Use TypedDict instead of dict[str, Any] for known-structure dictionaries:
from typing import TypedDict
class UserRecord(TypedDict):
id: int
name: str
email: str
class PartialUser(TypedDict, total=False):
nickname: str
avatar_url: str
def create_user(data: UserRecord) -> None:
print(data["name"]) # mypy knows this is str
Protocols for Duck Typing
Prefer Protocol over concrete base classes for flexible interfaces:
from typing import Protocol
class Closeable(Protocol):
def close(self) -> None: ...
class Readable(Protocol):
def read(self, n: int = -1) -> bytes: ...
def process_stream(stream: Readable) -> bytes:
return stream.read()
Strictness Levels
Adopt strictness incrementally. Start permissive, tighten over time.
Recommended Progression
Level 1 — New project baseline (pyproject.toml):
[tool.mypy]
python_version = "3.12"
warn_return_any = true
warn_unused_configs = true
warn_unused_ignores = true
Level 2 — Growing codebase:
[tool.mypy]
python_version = "3.12"
warn_return_any = true
warn_unused_configs = true
warn_unused_ignores = true
disallow_incomplete_defs = true
check_untyped_defs = true
Level 3 — Strict (production-ready):
[tool.mypy]
python_version = "3.12"
strict = true
warn_unused_ignores = true
strict enables: disallow_untyped_defs, disallow_any_generics, warn_return_any, no_implicit_reexport, strict_equality, and more.
Handling Third-Party Libraries
Install Type Stubs
# Check if stubs are available
mypy --install-types
# Install specific stubs
pip install types-requests types-PyYAML types-redis
Suppress Missing Stubs Per Module
Avoid global ignore_missing_imports. Scope it to specific libraries:
# pyproject.toml — preferred approach
[[tool.mypy.overrides]]
module = ["boto3.*", "botocore.*", "some_untyped_lib"]
ignore_missing_imports = true
Inline Suppression (Use Sparingly)
import untyped_lib # type: ignore[import-untyped]
result = complex_dynamic_call() # type: ignore[no-any-return] # reason: third-party returns Any
Always add a comment explaining why the suppression is necessary.
Common Use Cases
Migrating an Existing Codebase
Start with zero annotations and progressively type the codebase:
- Run
mypy --ignore-missing-imports src/— establish a zero-error baseline. - Enable
check_untyped_defs = trueto catch bugs inside unannotated functions. - Add annotations to public API functions first (highest value).
- Enable
disallow_incomplete_defsto prevent partially-annotated functions. - Gradually enable
disallow_untyped_defsper package via[[tool.mypy.overrides]]. - Reach
strict = trueas the final goal.
# Migrate package-by-package
[[tool.mypy.overrides]]
module = "myapp.core.*"
disallow_untyped_defs = true
[[tool.mypy.overrides]]
module = "myapp.legacy.*"
ignore_errors = true # deal with later
FastAPI / Pydantic Integration
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
tags: list[str] = []
# FastAPI + mypy works seamlessly — Pydantic models are fully typed
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def get_item(item_id: int) -> Item:
return Item(name="Widget", price=9.99)
SQLAlchemy with mypy Plugin
[tool.mypy]
plugins = ["sqlalchemy.ext.mypy.plugin"]
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
email: Mapped[str | None]
Type Narrowing
Mypy understands isinstance, assert, and guard patterns:
def render(value: str | int | None) -> str:
if value is None:
return ""
if isinstance(value, int):
return str(value)
return value.upper() # mypy knows: value is str here
Use reveal_type() during development to inspect inferred types:
x = [1, 2, 3]
reveal_type(x) # Revealed type is "builtins.list[builtins.int]"
CI Integration
Pre-commit Hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.9.0
hooks:
- id: mypy
additional_dependencies:
- types-requests
- pydantic
GitHub Actions
- name: Run mypy
run: mypy src/ --config-file pyproject.toml
Quick Reference
| Pattern | Syntax |
|---|---|
| Basic function | def f(x: int) -> str: |
| Optional param | x: str | None = None |
| List of strings | list[str] |
| Dict | dict[str, int] |
| Callable | Callable[[int, str], bool] |
| Any iterable | Iterable[str] |
| Typed dict | class X(TypedDict): key: type |
| Protocol | class X(Protocol): def method(...): |
| Suppress line | # type: ignore[error-code] |
| Debug type | reveal_type(expr) |
| Forward ref | from __future__ import annotations |
| Flag | Purpose |
|---|---|
strict |
Enable all optional checks |
disallow_untyped_defs |
Require all function annotations |
check_untyped_defs |
Check bodies of unannotated functions |
warn_return_any |
Warn on implicit Any returns |
warn_unused_ignores |
Catch stale # type: ignore comments |
ignore_missing_imports |
Suppress missing stub errors |
Additional Resources
For complete configuration options and advanced patterns:
references/configuration-guide.md— Completepyproject.tomltemplates and per-module override patternsreferences/type-patterns.md— Generics, Protocols, TypeVar, ParamSpec, overloads, and advanced patternsexamples/pyproject.toml— Ready-to-use configuration for new and migrating projects