| name | mypy |
| description | mypy - Static type checker for Python with gradual typing, strict mode, Protocol support, and framework integration |
| version | 1.0.0 |
| category | toolchain |
| author | Claude MPM Team |
| license | MIT |
| progressive_disclosure | {"entry_point":{"summary":"Static type checker for Python with gradual typing and strict mode","when_to_use":"Adding type safety to Python projects, using type hints in FastAPI/Django, enforcing type checking in CI/CD, refactoring to add type annotations","quick_start":"1. pip install mypy 2. Add type hints to your code 3. mypy your_module.py 4. Create mypy.ini for configuration 5. Use --strict for maximum safety"}} |
| context_limit | 700 |
| tags | ["mypy","type-checking","type-hints","static-analysis","python","fastapi","django"] |
| requires_tools | [] |
mypy - Static Type Checking for Python
Overview
mypy is the standard static type checker for Python, enabling gradual typing with type hints (PEP 484) and comprehensive type safety. It catches type errors before runtime, improves code documentation, and enhances IDE support while maintaining Python's dynamic nature through incremental adoption.
Key Features:
- Gradual typing: Add types incrementally to existing code
- Strict mode: Maximum type safety with --strict flag
- Type inference: Automatically infer types from context
- Protocol support: Structural typing (duck typing with types)
- Generic types: TypeVar, Generic, and advanced type patterns
- Framework integration: FastAPI, Django, Pydantic compatibility
- Plugin system: Extend type checking for libraries
- Incremental checking: Fast type checking on large codebases
Installation:
pip install mypy
pip install mypy types-requests types-PyYAML types-redis
pip install mypy pydantic
pip install mypy django-stubs
pip install mypy pre-commit
Type Annotation Basics
1. Variable Type Hints
name: str = "Alice"
age: int = 30
height: float = 5.9
is_active: bool = True
count = 10
message = "Hello"
from typing import Union
user_id: Union[int, str] = 123
result: Union[int, None] = None
from typing import Optional
user_email: Optional[str] = None
2. Function Type Hints
def greet(name: str) -> str:
return f"Hello, {name}"
def add(a: int, b: int) -> int:
return a + b
def create_user(name: str, age: int = 18) -> dict:
return {"name": name, "age": age}
def log_message(message: str) -> None:
print(message)
from typing import NoReturn
def raise_error() -> NoReturn:
raise ValueError("Always raises")
3. Collection Type Hints
from typing import List, Dict, Set, Tuple
numbers: List[int] = [1, 2, 3, 4]
names: List[str] = ["Alice", "Bob", "Charlie"]
user_ages: Dict[str, int] = {"Alice": 30, "Bob": 25}
config: Dict[str, Union[str, int]] = {"host": "localhost", "port": 8000}
unique_ids: Set[int] = {1, 2, 3}
coordinate: Tuple[float, float] = (10.5, 20.3)
user_record: Tuple[int, str, bool] = (1, "Alice", True)
numbers: Tuple[int, ...] = (1, 2, 3, 4, 5)
numbers: list[int] = [1, 2, 3]
user_ages: dict[str, int] = {"Alice": 30}
4. Class Type Hints
class User:
name: str
age: int
email: Optional[str]
def __init__(self, name: str, age: int, email: Optional[str] = None) -> None:
self.name = name
self.age = age
self.email = email
def get_info(self) -> Dict[str, Union[str, int]]:
return {
"name": self.name,
"age": self.age,
"email": self.email or "N/A"
}
@classmethod
def from_dict(cls, data: Dict[str, any]) -> "User":
return cls(
name=data["name"],
age=data["age"],
email=data.get("email")
)
Advanced Type Hints
1. Literal Types
from typing import Literal
def set_log_level(level: Literal["debug", "info", "warning", "error"]) -> None:
print(f"Log level: {level}")
set_log_level("debug")
set_log_level("error")
set_log_level("verbose")
Status = Literal["pending", "approved", "rejected"]
def update_status(status: Status) -> None:
pass
2. Type Aliases
from typing import Dict, List, Union
UserId = int
UserName = str
def get_user(user_id: UserId) -> UserName:
return f"User {user_id}"
JSON = Union[Dict[str, "JSON"], List["JSON"], str, int, float, bool, None]
Headers = Dict[str, str]
QueryParams = Dict[str, Union[str, int, List[str]]]
def make_request(
url: str,
headers: Headers,
params: QueryParams
) -> JSON:
pass
from typing import NewType
UserId = NewType("UserId", int)
ProductId = NewType("ProductId", int)
def get_user(user_id: UserId) -> str:
return f"User {user_id}"
user = UserId(123)
product = ProductId(456)
get_user(user)
get_user(product)
3. Generics and TypeVar
from typing import TypeVar, Generic, List
T = TypeVar("T")
def first_element(items: List[T]) -> T:
return items[0]
num = first_element([1, 2, 3])
name = first_element(["Alice", "Bob"])
from typing import Union
NumericType = TypeVar("NumericType", int, float)
def add_numbers(a: NumericType, b: NumericType) -> NumericType:
return a + b
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()
def is_empty(self) -> bool:
return len(self._items) == 0
int_stack: Stack[int] = Stack()
int_stack.push(42)
int_stack.push("hello")
str_stack: Stack[str] = Stack()
str_stack.push("hello")
4. Protocol (Structural Typing)
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> str:
...
class Circle:
def draw(self) -> str:
return "Drawing circle"
class Square:
def draw(self) -> str:
return "Drawing square"
def render(obj: Drawable) -> str:
return obj.draw()
circle = Circle()
square = Square()
render(circle)
render(square)
from typing import runtime_checkable
@runtime_checkable
class Closeable(Protocol):
def close(self) -> None:
...
class File:
def close(self) -> None:
pass
f = File()
isinstance(f, Closeable)
5. Callable Types
from typing import Callable
def apply_twice(func: Callable[[int], int], value: int) -> int:
return func(func(value))
def double(x: int) -> int:
return x * 2
result = apply_twice(double, 5)
from typing import TypeVar
T = TypeVar("T")
R = TypeVar("R")
def map_values(
func: Callable[[T], R],
values: List[T]
) -> List[R]:
return [func(v) for v in values]
Validator = Callable[[str, int], bool]
def validate_user(name: str, age: int) -> bool:
return len(name) > 0 and age >= 0
validator: Validator = validate_user
mypy Configuration
1. mypy.ini Configuration
[mypy]
python_version = 3.11
files = src,tests
exclude = build,dist,venv
disallow_untyped_defs = True
disallow_any_unimported = False
no_implicit_optional = True
warn_return_any = True
warn_unused_ignores = True
warn_redundant_casts = True
show_error_codes = True
show_column_numbers = True
pretty = True
incremental = True
cache_dir = .mypy_cache
[mypy-tests.*]
disallow_untyped_defs = False
[mypy-migrations.*]
ignore_errors = True
[mypy-redis.*]
ignore_missing_imports = True
[mypy-celery.*]
ignore_missing_imports = True
2. pyproject.toml Configuration
[tool.mypy]
python_version = "3.11"
files = ["src", "tests"]
exclude = ["build", "dist", "venv"]
disallow_untyped_defs = true
disallow_any_unimported = false
no_implicit_optional = true
warn_return_any = true
warn_unused_ignores = true
warn_redundant_casts = true
strict_equality = true
strict_concatenate = true
show_error_codes = true
show_column_numbers = true
pretty = true
color_output = true
incremental = true
cache_dir = ".mypy_cache"
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = ["redis.*", "celery.*"]
ignore_missing_imports = true
3. Strict Mode
mypy --strict src/
mypy \
--disallow-any-unimported \
--disallow-any-expr \
--disallow-any-decorated \
--disallow-any-explicit \
--disallow-any-generics \
--disallow-subclassing-any \
--disallow-untyped-calls \
--disallow-untyped-defs \
--disallow-incomplete-defs \
--check-untyped-defs \
--disallow-untyped-decorators \
--no-implicit-optional \
--warn-redundant-casts \
--warn-unused-ignores \
--warn-return-any \
--warn-unreachable \
--strict-equality \
src/
[mypy]
strict = True
disallow_any_expr = False
disallow_any_explicit = False
Incremental Adoption Strategies
1. Start with Entry Points
from typing import Optional
from app.services import UserService
def main(config_path: Optional[str] = None) -> None:
"""Application entry point."""
service = UserService()
service.run()
if __name__ == "__main__":
main()
mypy main.py
mypy main.py app/services.py
mypy src/
2. Per-Module Strict Mode
[mypy]
ignore_missing_imports = True
disallow_untyped_defs = False
[mypy-app.services.user_service]
disallow_untyped_defs = True
warn_return_any = True
[mypy-app.api.*]
disallow_untyped_defs = True
no_implicit_optional = True
[mypy-app.legacy.*]
ignore_errors = True
3. Use # type: ignore Strategically
import legacy_module
def process_data(data):
return data.transform()
user_dict = get_user_dict()
user_id = user_dict["id"]
result = external_api.call()
4. Reveal Types During Development
from typing import reveal_type
def process_user(user_id: int):
user = get_user(user_id)
reveal_type(user)
name = user.name
reveal_type(name)
FastAPI Integration
1. FastAPI with Type Hints
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
app = FastAPI()
class User(BaseModel):
id: int
name: str
email: str
age: Optional[int] = None
class UserCreate(BaseModel):
name: str
email: str
age: Optional[int] = None
@app.get("/")
def read_root() -> dict[str, str]:
return {"message": "Hello World"}
@app.get("/users/{user_id}")
def read_user(user_id: int) -> User:
if user_id == 0:
raise HTTPException(status_code=404, detail="User not found")
return User(id=user_id, name=f"User {user_id}", email="user@example.com")
@app.get("/users")
def list_users(skip: int = 0, limit: int = 10) -> List[User]:
users = [
User(id=i, name=f"User {i}", email=f"user{i}@example.com")
for i in range(skip, skip + limit)
]
return users
@app.post("/users")
def create_user(user: UserCreate) -> User:
return User(id=1, name=user.name, email=user.email, age=user.age)
2. Async FastAPI Type Checking
from typing import List, Optional
from fastapi import FastAPI, Depends
from sqlalchemy.ext.asyncio import AsyncSession
app = FastAPI()
async def get_db() -> AsyncSession:
async with AsyncSessionLocal() as session:
yield session
@app.get("/users/{user_id}")
async def read_user(
user_id: int,
db: AsyncSession = Depends(get_db)
) -> User:
user = await db.get(User, user_id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return user
@app.get("/users")
async def list_users(
skip: int = 0,
limit: int = 10,
db: AsyncSession = Depends(get_db)
) -> List[User]:
result = await db.execute(
select(User).offset(skip).limit(limit)
)
return result.scalars().all()
3. FastAPI Dependency Injection Types
from typing import Annotated, Optional
from fastapi import FastAPI, Depends, Header, HTTPException
app = FastAPI()
async def get_current_user(
authorization: Annotated[Optional[str], Header()] = None
) -> User:
if authorization is None:
raise HTTPException(status_code=401, detail="Not authenticated")
return User(id=1, name="Current User", email="user@example.com")
@app.get("/me")
async def read_current_user(
current_user: Annotated[User, Depends(get_current_user)]
) -> User:
return current_user
class UserService:
def __init__(self, db: AsyncSession) -> None:
self.db = db
async def get_user(self, user_id: int) -> Optional[User]:
return await self.db.get(User, user_id)
def get_user_service(
db: Annotated[AsyncSession, Depends(get_db)]
) -> UserService:
return UserService(db)
@app.get("/users/{user_id}")
async def get_user_endpoint(
user_id: int,
service: Annotated[UserService, Depends(get_user_service)]
) -> User:
user = await service.get_user(user_id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return user
Django Integration
1. Django with django-stubs
pip install django-stubs mypy
python -m mypy --install-types
[mypy]
plugins = mypy_django_plugin.main
[mypy.plugins.django-stubs]
django_settings_module = "myproject.settings"
2. Django Models with Type Hints
from django.db import models
from typing import Optional
class User(models.Model):
email: models.EmailField = models.EmailField(unique=True)
name: models.CharField = models.CharField(max_length=100)
age: models.IntegerField = models.IntegerField(null=True, blank=True)
is_active: models.BooleanField = models.BooleanField(default=True)
created_at: models.DateTimeField = models.DateTimeField(auto_now_add=True)
def get_display_name(self) -> str:
return self.name or self.email
@classmethod
def get_active_users(cls) -> models.QuerySet["User"]:
return cls.objects.filter(is_active=True)
3. Django Views with Type Hints
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404
from typing import Dict, Any
from .models import User
def user_detail(request: HttpRequest, user_id: int) -> JsonResponse:
user: User = get_object_or_404(User, pk=user_id)
data: Dict[str, Any] = {
"id": user.id,
"name": user.name,
"email": user.email,
}
return JsonResponse(data)
def user_list(request: HttpRequest) -> JsonResponse:
users = User.get_active_users()
data = {
"users": list(users.values("id", "name", "email"))
}
return JsonResponse(data)
Type Stubs and Third-Party Libraries
1. Installing Type Stubs
pip install types-requests
pip install types-PyYAML
pip install types-redis
pip install types-boto3
pip search types-
mypy --install-types
2. Creating Custom Stubs
from typing import Optional, List
class Client:
def __init__(self, api_key: str) -> None: ...
def get_user(self, user_id: int) -> Optional[dict]: ...
def list_users(self, limit: int = 10) -> List[dict]: ...
def connect(host: str, port: int) -> Client: ...
[mypy]
mypy_path = stubs
3. Ignoring Missing Imports
[mypy-external_lib.*]
ignore_missing_imports = True
[mypy-celery.*,redis.*,boto3.*]
ignore_missing_imports = True
CI/CD Integration
1. GitHub Actions
name: Type Check
on: [push, pull_request]
jobs:
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install mypy
pip install -r requirements.txt
pip install types-requests types-PyYAML
- name: Run mypy
run: mypy src/
- name: Run mypy strict on new code
run: mypy --strict src/api/
2. Pre-commit Hook
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
hooks:
- id: mypy
args: [--strict, --ignore-missing-imports]
additional_dependencies:
- types-requests
- types-PyYAML
pip install pre-commit
pre-commit install
pre-commit run mypy --all-files
3. Make Target
.PHONY: typecheck
typecheck:
mypy src/
.PHONY: typecheck-strict
typecheck-strict:
mypy --strict src/
.PHONY: typecheck-report
typecheck-report:
mypy src/ --html-report mypy-report
@echo "Report: mypy-report/index.html"
.PHONY: ci
ci: typecheck test lint
Common Patterns and Idioms
1. Overload for Multiple Signatures
from typing import overload, Union
@overload
def process(data: str) -> str: ...
@overload
def process(data: int) -> int: ...
@overload
def process(data: list) -> list: ...
def process(data: Union[str, int, list]) -> Union[str, int, list]:
"""Process different data types."""
if isinstance(data, str):
return data.upper()
elif isinstance(data, int):
return data * 2
else:
return [x * 2 for x in data]
result1: str = process("hello")
result2: int = process(42)
result3: str = process(42)
2. TypedDict for Structured Dicts
from typing import TypedDict, Optional
class UserDict(TypedDict):
id: int
name: str
email: str
age: Optional[int]
def create_user(data: UserDict) -> UserDict:
return {
"id": 1,
"name": data["name"],
"email": data["email"],
"age": data.get("age"),
}
user: UserDict = {
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"age": 30
}
invalid_user: UserDict = {
"id": 1,
"name": "Bob",
}
3. Final and Constant Values
from typing import Final
API_VERSION: Final = "v1"
MAX_RETRIES: Final[int] = 3
API_VERSION = "v2"
from typing import final
@final
class BaseConfig:
pass
class AppConfig(BaseConfig):
pass
4. Self Type for Method Chaining
from typing import Self
class Builder:
def __init__(self) -> None:
self._value = 0
def add(self, value: int) -> Self:
self._value += value
return self
def multiply(self, value: int) -> Self:
self._value *= value
return self
def build(self) -> int:
return self._value
result = Builder().add(5).multiply(2).add(3).build()
mypy vs pyright Comparison
Feature Comparison
| Feature | mypy | pyright |
|---|
| Type Checker | Official Python type checker | Microsoft's type checker |
| Speed | Slower on large codebases | Faster, incremental |
| Strictness | Configurable strict mode | Very strict by default |
| IDE Integration | Good (LSP support) | Excellent (Pylance in VS Code) |
| Plugin System | Yes (mypy plugins) | Limited |
| Error Messages | Clear, detailed | Very detailed, helpful |
| Community | Large, official | Growing, Microsoft-backed |
| Type Inference | Good | Excellent |
| Configuration | mypy.ini, pyproject.toml | pyrightconfig.json, pyproject.toml |
When to Use mypy
When to Use pyright
Using Both
[tool.mypy]
strict = true
files = ["src"]
[tool.pyright]
include = ["src"]
strict = ["src/api"]
reportMissingTypeStubs = false
mypy src/
pyright src/
Best Practices
1. Start with Key Modules
from typing import Optional
class UserService:
def get_user(self, user_id: int) -> Optional[User]:
"""Retrieve user by ID."""
return self.db.query(User).get(user_id)
def create_user(self, data: UserCreate) -> User:
"""Create new user."""
user = User(**data.dict())
self.db.add(user)
self.db.commit()
return user
2. Use Type Aliases for Readability
from typing import Dict, List, Union
JSON = Union[Dict[str, "JSON"], List["JSON"], str, int, float, bool, None]
Headers = Dict[str, str]
UserId = int
def parse_response(data: JSON, headers: Headers) -> UserId:
pass
def parse_response(
data: Union[Dict[str, Union[...]], List[...], str, int, float, bool, None],
headers: Dict[str, str]
) -> int:
pass
3. Prefer Explicit Over Implicit
def get_user(user_id: int) -> Optional[User]:
return db.query(User).get(user_id)
def _format_name(first, last):
return f"{first} {last}"
4. Use reveal_type for Debugging
from typing import reveal_type
def process_data(data):
result = transform(data)
reveal_type(result)
return result * 2
5. Document Type Ignores
import legacy_module
import legacy_module
Common Pitfalls
❌ Anti-Pattern 1: Using Any Everywhere
from typing import Any
def process(data: Any) -> Any:
return data.transform()
Correct:
from typing import Union, Protocol
class Transformable(Protocol):
def transform(self) -> dict: ...
def process(data: Transformable) -> dict:
return data.transform()
❌ Anti-Pattern 2: Ignoring Type Errors Globally
[mypy]
ignore_errors = True
Correct:
[mypy-legacy.*]
ignore_errors = True
[mypy]
strict = True
❌ Anti-Pattern 3: Not Using Optional
def get_user(user_id: int) -> User:
user = db.get(user_id)
return user
Correct:
from typing import Optional
def get_user(user_id: int) -> Optional[User]:
return db.get(user_id)
user = get_user(123)
if user is not None:
print(user.name)
Quick Reference
Common Commands
mypy main.py
mypy src/
mypy --strict src/
mypy --install-types
mypy src/ --html-report mypy-report
mypy --show-error-codes src/
mypy --ignore-missing-imports src/
mypy --follow-imports=silent src/
mypy --incremental src/
mypy --verbose src/
Error Code Reference
[attr-defined]
[arg-type]
[return-value]
[assignment]
[call-overload]
[index]
[operator]
[import]
[misc]
[no-untyped-def]
[var-annotated]
Resources
Related Skills
When using mypy, consider these complementary skills (available in the skill library):
- pytest: Type-safe testing with mypy - integrates type checking into your test suite for comprehensive type coverage
- fastapi-local-dev: FastAPI with full type safety - combines FastAPI's runtime validation with mypy's static checking
- pydantic: Runtime type validation with mypy support - validates data at runtime while mypy validates at compile time
mypy Version Compatibility: This skill covers mypy 1.8+ and reflects current best practices for Python type checking in 2025.