Линтеры и типы: ruff, black, mypy, pre-commit
В команде разработчиков легко получить непоследовательный стиль кода: разные отступы, импорты в случайном порядке, опечатки в именах. Решение - автоматические инструменты: форматтер для единого стиля, линтер для обнаружения проблем, type checker для type safety, pre-commit hooks для автозапуска перед коммитом. В этом уроке - современный стек: ruff, black, mypy.
Зачем нужны эти инструменты
Без них:
- Код-ревью тратится на «поправь форматирование»
- Опечатки в именах переменных всплывают в production
- Несоответствие type hints обнаруживается в runtime
- Каждый разработчик использует свой стиль
С ними:
- Форматирование автоматическое, не предмет спора
- Многие баги ловятся ДО запуска кода
- Type checking предупреждает о несоответствиях
- Стиль кода одинаков во всём проекте
ruff - быстрый линтер и форматтер
ruff от Astral - универсальный инструмент с компилируемым ядром:
pip install ruff
ruff check . # линтинг
ruff check . --fix # авто-фиксы где возможно
ruff format . # форматирование (как black)
ruff check --select=E,F,W # выбор правил
ruff в 10-100 раз быстрее старых инструментов (flake8, pylint, isort, black отдельно). Заменяет многие из них. В 2026 году ruff де-факто стандарт для новых проектов.
Конфигурация ruff
# pyproject.toml
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # pyflakes (undefined names, unused imports)
"W", # pycodestyle warnings
"I", # isort (import order)
"N", # pep8-naming
"B", # flake8-bugbear (common bugs)
"UP", # pyupgrade (modernize syntax)
"SIM", # flake8-simplify
"RUF", # ruff-specific
]
ignore = [
"E501", # line too long (handled by formatter)
"B008", # function call in default arguments (FastAPI compatibility)
]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"] # assert разрешён в тестах
ruff поддерживает большинство правил из других tools: pyflakes, pycodestyle, isort, pyupgrade, flake8-bugbear, и многое другое. Один конфиг - вся проверка. Живёт он в том же pyproject.toml, что и метаданные пакета.
ruff format - замена black
ruff format . # форматирование всех .py файлов
ruff format --check . # проверка без изменений (для CI)
ruff format совместим с black по семантике, но быстрее. С 2024 года считается готовым для production. Если у тебя был black, переход на ruff format безболезненный.
black - "uncompromising formatter"
pip install black
black . # отформатирует все .py файлы
black --check . # для CI - не меняет, упадёт если нужно
Black форматирует код по строгим правилам - почти нет конфигурации. Это его философия: "the only formatting choice is no choice". Один стиль для всего сообщества.
С приходом ruff многие переходят, но black всё ещё активно используется и стабилен.
isort - порядок импортов
pip install isort
isort . # сортирует импорты
Группирует и сортирует:
- Стандартная библиотека
- Сторонние пакеты
- Локальные модули
С blank line между группами, alphabetical внутри. ruff includes isort правила (через I selector), поэтому отдельно isort часто не нужен.
mypy - static type checker
pip install mypy
mypy myapp/ # проверка типов
Анализирует код через type hints и находит несоответствия:
def greet(name: str) -> str:
return f"Hello, {name}"
greet(42) # mypy ERROR: Argument 1 has incompatible type "int"; expected "str"
Без mypy эта ошибка появится только в runtime - и то если функция действительно работает с числом неправильно. mypy ловит до запуска.
Конфигурация mypy
# pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = true # все возможные проверки
warn_unused_ignores = true
warn_unused_configs = true
no_implicit_optional = true # x: str = None требует Optional[str]
[[tool.mypy.overrides]]
module = "untyped_library.*"
ignore_missing_imports = true # для библиотек без type hints
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false # тесты могут без типов
strict = true включает все строгие проверки. Это рекомендуется для нового кода. Для legacy кода можно постепенно включать через отдельные настройки.
Type hints essentials
from typing import Optional, Union, List, Dict, Callable, Any, TypeVar, Generic
# Базовое
def add(a: int, b: int) -> int:
return a + b
# Optional (может быть None)
def find(id: int) -> Optional[User]:
...
# Современный синтаксис Py 3.10+
def find(id: int) -> User | None:
...
# Collections (Py 3.9+)
def process(items: list[str]) -> dict[str, int]:
...
# Callable
on_done: Callable[[int], None]
# Generic
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
Подробно разбирали в уроке про type hints. Помни: type hints для mypy и читаемости, не runtime проверка.
pre-commit hooks
pre-commit это framework для автозапуска проверок перед git commit:
pip install pre-commit
Конфиг .pre-commit-config.yaml:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
additional_dependencies: [types-requests]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
Установка:
pre-commit install # устанавливает git hook
Теперь перед каждым git commit:
- ruff проверит и форматирует
- mypy проверит типы
- Стандартные хуки проверят whitespace, yaml, etc
Если что-то fails - commit блокируется. Это гарантирует что в репозитории всегда чистый код.
Запуск без коммита
pre-commit run --all-files # все файлы
pre-commit run ruff # только ruff
Полезно для проверки после refactoring или для CI.
Coverage с pytest
pip install pytest-cov
pytest --cov=myapp --cov-report=html --cov-report=term
# с порогом
pytest --cov=myapp --cov-fail-under=80
Создаёт HTML отчёт показывающий какие строки покрыты тестами. --cov-fail-under валит build если coverage ниже порога. Обычная цель 80%+ для backend.
Не гонись за 100% - есть код который тяжело тестировать и не критичен (логирование, простые getters). Лучше 80% покрытия с хорошими тестами, чем 100% с поверхностными.
CI/CD пример
GitHub Actions:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install
run: pip install -e ".[dev]"
- name: Lint
run: ruff check .
- name: Format check
run: ruff format --check .
- name: Type check
run: mypy myapp/
- name: Tests
run: pytest --cov=myapp --cov-fail-under=80
Каждый push/PR прогоняет: линт, формат, типы, тесты с coverage. Если что-то fails - PR блокируется до фикса.
Другие полезные инструменты
| Tool | Назначение |
|---|---|
| pylint | Глубокий линтер (медленнее ruff, больше правил) |
| bandit | Security-проверки (SQL injection, hardcoded secrets) |
| pyright | Type checker от Microsoft, основа Pylance в VS Code |
| pyupgrade | Авто-модернизация syntax под новые Python |
| deptry | Проверка unused/missing dependencies |
| vulture | Поиск dead code (неиспользуемые функции) |
В современных проектах часто: ruff + mypy + pre-commit + pytest. Этого достаточно для большинства задач.
Соотношение тестов и других проверок
Каждый инструмент ловит свой класс багов:
| Уровень | Что ловит |
|---|---|
| Formatter (ruff format) | Стиль кода |
| Linter (ruff check) | Опечатки, неиспользуемые импорты, common bugs |
| Type checker (mypy) | Type mismatch, неправильные параметры |
| Unit tests | Поведение функций |
| Integration tests | Взаимодействие компонентов |
| E2E tests | Полные сценарии пользователя |
Лучше всего иметь все уровни. Type check сильно сокращает количество багов которые надо ловить тестами.
Performance comparison
| Tool | Скорость на 10K строк |
|---|---|
| ruff check | ~50ms |
| black | ~1s |
| flake8 | ~3s |
| pylint | ~30s |
| mypy | ~5s |
ruff меняет правила игры - можно запускать на каждом save в редакторе без задержек. Раньше с flake8 или pylint это было slow.
Распространённые ошибки
1. Игнорировать линтер вместо фикса
import unused_module # noqa: F401 - игнорируем
noqa для исключений (рассмотрены), но злоупотребление превращает линтер в косметику.
2. Type hints без проверки mypy
def add(a: int, b: int) -> int:
return a + b + "string" # runtime ошибка
Type hints без mypy не проверяются. Если есть hints - запускай mypy.
3. Pre-commit без CI проверки
Pre-commit можно обойти git commit --no-verify. В CI тоже должна быть проверка - страховка.
4. Coverage без хороших тестов
def test_dummy():
func() # вызывает но не проверяет результат
100% coverage с такими тестами обманчиво. Coverage показывает что строки выполнились, не что поведение правильное.
5. Чрезмерно строгий mypy в legacy
mypy --strict legacy_project/ # 1000+ ошибок
Для legacy лучше постепенно: сначала без strict, потом включать по одной проверке. Иначе фронт работы огромный.
Хороший минимум для нового проекта
# pyproject.toml
[project.optional-dependencies]
dev = [
"ruff>=0.4",
"mypy>=1.10",
"pytest>=8.0",
"pytest-cov>=5.0",
"pre-commit>=3.7",
]
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "B", "UP", "SIM", "RUF"]
[tool.mypy]
python_version = "3.12"
strict = true
[tool.pytest.ini_options]
testpaths = ["tests"]
Плюс .pre-commit-config.yaml с ruff и mypy. Это база для качественного backend проекта.
Сравнение с Go
В Go встроенные инструменты:
gofmt -w . # форматирование (как black)
go vet ./... # линтинг
golint ./... # стиль (deprecated, использовать golangci-lint)
go test ./... # тесты
Плюс популярный golangci-lint aggregator. У Go всё интегрировано в toolchain. У Python нужно собирать стек из отдельных инструментов, но они мощнее и более конфигурируемы.
Мини-задание
- Setup для нового проекта:
mkdir my-project && cd my-project
pip install ruff mypy pytest pytest-cov
# pyproject.toml
cat > pyproject.toml <<EOF
[tool.ruff]
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "W", "I", "B", "UP"]
[tool.mypy]
strict = true
EOF
# проверка
ruff check .
ruff format .
mypy myapp/
pytest --cov=myapp
- Pre-commit:
pip install pre-commit
# .pre-commit-config.yaml
cat > .pre-commit-config.yaml <<EOF
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.0
hooks:
- id: ruff
- id: ruff-format
EOF
pre-commit install
git add .
git commit -m "test" # запустится ruff проверка
- Type checking:
# mypy_demo.py
def greet(name: str) -> str:
return f"Hello, {name}"
greet(42) # mypy ERROR
mypy mypy_demo.py
# mypy_demo.py:5: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
Что дальше
Модуль 9 завершён. Освоили тестирование с pytest, fixtures, mocking, и tooling для качества. У тебя есть полный набор инструментов для разработки качественного backend кода. В следующем модуле перейдём к веб-бэкенду: WSGI и ASGI, FastAPI, Pydantic, SQLAlchemy и всё что нужно для production REST API.