Аннотации типов в Python -- это метаданные, которые интерпретатор не проверяет. Для реальной проверки нужны внешние инструменты: mypy и pyright. Они анализируют код статически (без запуска) и находят ошибки типизации.
mypy -- стандарт проверки типов
mypy -- первый и наиболее распространенный статический анализатор типов для Python. Создан Юкка Лехтосало в Dropbox.
Установка и запуск
# Install
pip install mypy
# or
uv add --dev mypy
# Check a single file
mypy main.py
# Check an entire project
mypy src/
# Check with specific Python version
mypy --python-version 3.14 src/
Пример проверки
# example.py
def greet(name: str) -> str:
return f"Hello, {name}!"
def add(a: int, b: int) -> int:
return a + b
# Errors that mypy will catch:
result: int = greet("Alice") # Error: str assigned to int
add("1", "2") # Error: str arguments to int parameters
$ mypy example.py
example.py:8: error: Incompatible types in assignment
(expression has type "str", variable has type "int")
example.py:9: error: Argument 1 to "add" has incompatible type "str";
expected "int"
Found 2 errors in 1 file
Конфигурация mypy
pyproject.toml
[tool.mypy]
python_version = "3.14"
strict = true
# What strict mode enables:
# warn_return_any = true
# warn_unused_configs = true
# disallow_untyped_defs = true
# disallow_incomplete_defs = true
# check_untyped_defs = true
# disallow_untyped_decorators = true
# no_implicit_optional = true
# warn_redundant_casts = true
# warn_unused_ignores = true
# warn_no_return = true
# no_implicit_reexport = true
# strict_equality = true
# Additional useful options
warn_unreachable = true
show_error_codes = true
pretty = true
# Packages to check
packages = ["mypackage"]
# Per-module overrides
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = "third_party_lib.*"
ignore_missing_imports = true
Поэтапное внедрение
Для существующих проектов включайте строгость постепенно:
# Stage 1: Basic checks
[tool.mypy]
python_version = "3.14"
warn_return_any = true
show_error_codes = true
# Stage 2: Require types for new code
disallow_untyped_defs = true
check_untyped_defs = true
# Stage 3: Full strict mode
strict = true
pyright -- быстрая альтернатива
pyright -- анализатор от Microsoft, написанный на TypeScript. Работает значительно быстрее mypy и является основой для Pylance (расширение VS Code).
Установка и запуск
# Install
pip install pyright
# or via npm (faster updates)
npm install -g pyright
# Run
pyright src/
pyright --pythonversion 3.14 src/
Конфигурация pyright
# pyproject.toml
[tool.pyright]
pythonVersion = "3.14"
typeCheckingMode = "strict"
include = ["src"]
exclude = ["**/node_modules", "**/__pycache__"]
# Type stub search paths
stubPath = "stubs"
reportMissingImports = true
reportMissingTypeStubs = false
reportGeneralClassIssues = true
reportOptionalMemberAccess = true
Сравнение mypy и pyright
| Аспект | mypy | pyright |
|---|---|---|
| Язык реализации | Python | TypeScript |
| Скорость | Медленнее | Быстрее (5-10x) |
| IDE-интеграция | Плагины | Pylance (VS Code) |
| Строгость | strict mode | basic/standard/strict |
| Type narrowing | Хороший | Превосходный |
| Совместимость | Стандарт де-факто | Растущая популярность |
| Инкрементальная проверка | Да (dmypy) | Встроенная |
Типичные ошибки и их исправление
Ошибка: Missing return type
# Error: Function is missing a return type annotation
def process(data):
return data.strip()
# Fix: Add return type
def process(data: str) -> str:
return data.strip()
Ошибка: Optional без проверки
# Error: Item "None" of "Optional[str]" has no attribute "upper"
def get_name() -> str | None:
return None
name = get_name()
# print(name.upper()) # Error!
# Fix 1: Check for None
if name is not None:
print(name.upper()) # OK
# Fix 2: Assert
assert name is not None
print(name.upper())
# Fix 3: Default value
print((name or "Unknown").upper())
Ошибка: Incompatible types
from typing import Any
# Error: Dict has incompatible value type
def process_config(config: dict[str, str]) -> None:
pass
data: dict[str, Any] = {"key": "value", "num": 42}
# process_config(data) # Error: dict[str, Any] not compatible
# Fix: Validate and narrow types
if all(isinstance(v, str) for v in data.values()):
process_config(data) # type: ignore[arg-type]
# Better fix: Use proper types from the start
typed_data: dict[str, str] = {"key": "value", "num": "42"}
process_config(typed_data)
Ошибка: Incompatible return type
# Error: Returning list[str | int] instead of list[str]
def get_names(items: list[dict]) -> list[str]:
result = [] # mypy infers list[str | int] from usage below
for item in items:
result.append(item.get("name", 0)) # 0 is int!
return result
# Fix: Proper default value
def get_names(items: list[dict]) -> list[str]:
result: list[str] = []
for item in items:
result.append(item.get("name", "Unknown"))
return result
type: ignore -- подавление ошибок
Иногда нужно подавить ложное срабатывание:
# Suppress specific error
value: int = some_dynamic_function() # type: ignore[assignment]
# Suppress all errors on a line
result = untyped_library.do_stuff() # type: ignore
# NEVER do this globally — fix the types instead
# BAD: Hundreds of type: ignore comments
# GOOD: Proper types and minimal ignores
# Best practice: always specify the error code
x = cast(str, value) # type: ignore[redundant-cast]
reveal_type -- отладка типов
x = [1, 2, 3]
reveal_type(x) # mypy: Revealed type is "builtins.list[builtins.int]"
d = {"a": 1, "b": "hello"}
reveal_type(d) # mypy: Revealed type is "builtins.dict[builtins.str, builtins.int | builtins.str]"
# Useful for debugging complex types
from collections.abc import Iterator
def gen() -> Iterator[int]:
yield 1
g = gen()
reveal_type(g) # Revealed type is "Iterator[int]"
cast -- приведение типов
cast не меняет значение в runtime, но сообщает анализатору тип:
from typing import cast
# When you know better than the type checker
data: object = get_from_cache("user")
user = cast(dict[str, str], data)
# Now type checker treats 'user' as dict[str, str]
# Use sparingly — if you need many casts, fix the source types
Stubs (.pyi файлы)
Stubs предоставляют типы для нетипизированных библиотек:
# mypackage.pyi — type stub file
def connect(host: str, port: int = ...) -> Connection: ...
class Connection:
def execute(self, query: str) -> list[dict[str, object]]: ...
def close(self) -> None: ...
# Install type stubs for popular libraries
pip install types-requests
pip install types-redis
pip install pandas-stubs
# Or use mypy to find missing stubs
mypy --install-types src/
Интеграция в CI/CD
Makefile
.PHONY: typecheck
typecheck:
mypy src/ --strict
@echo "Type checking passed!"
typecheck-report:
mypy src/ --strict --html-report reports/mypy
GitHub Actions
# .github/workflows/typecheck.yml
name: Type Check
on: [push, pull_request]
jobs:
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.14'
- run: pip install mypy
- run: mypy src/ --strict
Pre-commit hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
hooks:
- id: mypy
additional_dependencies: [types-requests]
args: [--strict]
Практический пример: типизация Flask-подобного приложения
from typing import Protocol, TypedDict
from collections.abc import Callable
from dataclasses import dataclass, field
class Request(TypedDict):
method: str
path: str
headers: dict[str, str]
body: str | None
@dataclass
class Response:
status: int
body: str
headers: dict[str, str] = field(default_factory=dict)
# Type alias for route handlers
type RouteHandler = Callable[[Request], Response]
class HasRoutes(Protocol):
"""Protocol for objects that have route registration."""
def route(self, path: str, method: str = "GET") -> Callable[[RouteHandler], RouteHandler]: ...
@dataclass
class App:
"""Minimal typed web framework."""
_routes: dict[tuple[str, str], RouteHandler] = field(default_factory=dict)
def route(self, path: str, method: str = "GET") -> Callable[[RouteHandler], RouteHandler]:
"""Register a route handler."""
def decorator(func: RouteHandler) -> RouteHandler:
self._routes[(method, path)] = func
return func
return decorator
def handle(self, request: Request) -> Response:
"""Dispatch request to appropriate handler."""
key = (request["method"], request["path"])
handler = self._routes.get(key)
if handler is None:
return Response(status=404, body="Not Found")
return handler(request)
# Usage — fully type-checked
app = App()
@app.route("/health")
def health(request: Request) -> Response:
return Response(status=200, body='{"status": "ok"}')
@app.route("/users", method="POST")
def create_user(request: Request) -> Response:
if request["body"] is None:
return Response(status=400, body="Body required")
return Response(status=201, body=request["body"])
# Type checker validates everything
response = app.handle({
"method": "GET",
"path": "/health",
"headers": {},
"body": None,
})
# response: Response — all attributes known to type checker