Я помню, как в одном стартапе мы потратили две недели на дебаг production-проблемы, которая была видна статическому анализатору с первого взгляда. Просто никто его не запускал. С тех пор я убеждён: проверка кода Python — это не опция, это базовая гигиена.
Python коварен. Он не скажет тебе о типе переменной до рантайма, не заметит неиспользуемый импорт, пока ты не запустишь код. Интерпретатор прощает многое, что в компилируемых языках ловится сразу. Поэтому проверка Python-кода — это то, что нужно автоматизировать на 100%.
Давай разберёмся, как это правильно делать.
Почему проверка кода в Python особенно критична
Вот честно: Python — это язык высокого доверия. Ты пишешь код, он работает. Красиво. Но в production'е начинаются сюрпризы.
Типичная ситуация: разработчик забыл про исключение, где-то опечатка в названии переменной, или функция возвращает None вместо списка. Код пройдёт через все тесты, потому что тесты не покрывают все edge-case'ы. И вот уже 3 часа ночи, а ты ловишь баг в production'е.
По моему опыту, 40% проблем, которые находит code review, — это ошибки, которые мог бы найти статический анализатор. Зачем тратить время людей?
Ещё важный момент: Python очень чувствителен к стилю. Если в команде каждый пишет по-своему — код становится нечитаемым. А нечитаемый код — это баги, которые никто не замечает.
Инструменты для статического анализа Python
Тут есть несколько игроков, у каждого своя фишка.
Pylint — это старенький, но серьёзный инструмент. Ловит всё: неиспользуемые переменные, отсутствующие импорты, нарушения PEP 8. Но иногда бывает шумным. На одном проекте мы отключили половину проверок, потому что они были слишком агрессивные.
pip install pylint
pylint your_module.py
Вывод будет примерно такой:
your_module.py:5:0: C0111: Missing module docstring (missing-docstring)
your_module.py:7:4: W0612: Unused variable 'x' (unused-variable)
your_module.py:10:0: C0103: Variable name "myVar" doesn't conform to snake_case naming style (invalid-name)
Flake8 — это мой фаворит для быстрой проверки. Комбинирует PyCodeStyle (проверка стиля) и PyFlakes (логические ошибки). Легче, чем Pylint, но ловит самое важное.
pip install flake8
flake8 your_module.py
your_module.py:2:1: F401 'os' imported but unused
your_module.py:5:1: E302 expected 2 blank lines, found 1
your_module.py:10:5: W503 line break before binary operator
Mypy — вот это штука. Это проверка типов. Если в коде есть type hints (подсказки типов), Mypy проверит, что ты не передаёшь строку где ожидается число.
def calculate_total(prices: list[float]) -> float:
return sum(prices)
# Mypy заметит эту ошибку:
result = calculate_total(["10", "20"]) # Error: Argument 1 to "calculate_total" has incompatible type
Запускается просто:
pip install mypy
mypy your_module.py
Black — это форматер, не анализатор. Но по-хорошему, это must-have. Он переписывает твой код, приводя его к единому стилю. Никаких споров о том, где ставить кавычки.
pip install black
black your_module.py
На одном проекте мы добавили Black в pre-commit hook, и количество замечаний на code review упало на 30%.
Пошаговая настройка проверки кода в CI/CD
Теория — это хорошо, но нужно это завести в production. Вот как я это обычно делаю.
Шаг 1: Создаём .flake8 конфиг в корне проекта
[flake8]
max-line-length = 120
exclude = .git,__pycache__,venv,migrations
ignore = E203, W503
per-file-ignores =
__init__.py:F401
tests/*:F841
Здесь я говорю Flake8:
- Строки до 120 символов — ок
- Игнорируем виртуальное окружение и миграции
- F401 (неиспользуемый импорт) — это ок в
__init__.py, потому что там мы экспортируем - В тестах можно иметь неиспользуемые переменные (F841)
Шаг 2: Создаём pyproject.toml для остальных инструментов
[tool.black]
line-length = 120
target-version = ['py310']
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false
ignore_missing_imports = true
[tool.isort]
profile = "black"
line_length = 120
Black и Mypy читают конфиг отсюда. Я специально не установил disallow_untyped_defs = true, потому что это слишком строго для существующих проектов.
Шаг 3: Добавляем в requirements-dev.txt
flake8==6.1.0
mypy==1.7.0
black==23.12.0
isort==5.13.2
pylint==3.0.3
Шаг 4: Создаём GitHub Actions workflow (или GitLab CI)
Если используешь GitHub:
name: Code Quality
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install -r requirements-dev.txt
- name: Run Black
run: black --check .
- name: Run isort
run: isort --check-only .
- name: Run Flake8
run: flake8 .
- name: Run Mypy
run: mypy .
Для GitLab CI:
code_quality:
stage: test
image: python:3.10
script:
- pip install -r requirements-dev.txt
- black --check .
- isort --check-only .
- flake8 .
- mypy .
only:
- merge_requests
Теперь каждый PR/MR будет проверяться автоматически. Если что-то не прошло — PR не сольётся.
Частые ошибки, которые ловит анализ кода Python
Когда я запускаю Flake8 на новом проекте, обычно вижу одно и то же:
Неиспользуемые импорты
import os # Забыли удалить
import sys
from typing import Optional
def get_config():
return {}
Flake8 скажет: F401 'os' imported but unused. Удаляем, и всё.
Ошибки в именах переменных
def calculate_price(quantity, unitPrice): # unitPrice нарушает PEP 8
return quantity * unitPrice
Должно быть unit_price. Mypy и Pylint это заметят.
Отсутствующие возвращаемые значения
def process_data(data: list[dict]) -> dict:
if not data:
return # Вернули None, а не dict!
return {"status": "ok"}
Mypy на это возмутится.
Потенциальные баги с None
def get_user_email(user_id: int) -> str | None:
# ...
return user.email # user может быть None
# Позже:
email = get_user_email(123)
email.lower() # Может упасть, если email это None
Mypy в strict mode это поймает и заставит тебя обработать None.
Слишком сложные функции
Pylint ловит функции, которые слишком сложные (циклическая сложность > 10). Это признак того, что функцию нужно разбить.
def complex_logic(a, b, c):
if a:
if b:
if c:
# ... много кода
if something:
# ... ещё много кода
Лучше разбить на несколько функций.
Динамический анализ vs статический
Тут важно понимать разницу. Статический анализ — это когда инструмент смотрит на текст кода. Динамический — это когда код запускается.
Статический анализ ловит:
- Синтаксические ошибки
- Неиспользуемые переменные
- Нарушения стиля
- Ошибки типов (если есть type hints)
- Потенциальные баги (вроде IndexError)
Динамический анализ ловит:
- Реальное поведение при определённых данных
- Утечки памяти
- Race conditions
- Баги, которые проявляются только в определённых условиях
Вывод: используй оба. Статический анализ — в CI/CD на каждый коммит. Динамический — это тесты (unit tests, integration tests).
На одном проекте мы писали код, который проходил все статические проверки, но на production'е валился из-за специфичного поведения базы данных. Тесты спасли нас.
Интеграция с IDE
Если ты используешь VS Code, установи расширение Python. Оно автоматически запустит Pylint/Flake8 прямо в редакторе.
Для PyCharm — встроена поддержка Pylint, PEP 8, и других инструментов. Просто включи в настройках.
Это экономит время: ты видишь ошибки сразу, ещё до того, как коммитишь.
Как не переусложнить
Важный момент: не надо включать все проверки подряд. Это приведёт к параличу разработки. На одном проекте мы включили Pylint со всеми проверками, и разработчики восстали.
Мой рецепт:
- Обязательно: Flake8 (базовые ошибки и стиль)
- Обязательно: Black (единый формат)
- Рекомендую: Mypy (если есть type hints)
- Опционально: Pylint (если хочешь больше контроля)
Начни с малого. Добавь Flake8 и Black в CI/CD. Дай команде привыкнуть. Потом добавь Mypy.
Если хочешь, чтобы проверка работала на 100% без ручного запуска — используй Distiq. Это AI code review, который интегрируется с GitHub, GitLab или GitVerse. Бот автоматически проверяет каждый MR/PR и оставляет комментарии с ошибками, проблемами безопасности и предложениями по оптимизации. Настраивается за 2 минуты, не требует сложной конфигурации. Работает, как команда опытных code reviewer'ов, но без их зарплаты.
