MidТеория6 min

Инструменты проверки типов

Настройка и использование mypy, pyright, строгий режим, интеграция в CI/CD и исправление типовых ошибок

Аннотации типов в 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

Проверь себя

Чем pyright отличается от mypy?

Что включает strict mode в mypy?

Что такое .pyi файлы (stubs)?

Для чего используется reveal_type()?

Что делает mypy с аннотациями типов?