Практическая настройка pre-commit для Python: линтеры, автоправки и контроль скорости сборки
Разберём, как собрать pre-commit-хук-пайплайн так, чтобы он реально повышал качество: ruff/format, запрет изменений в тестах, кэширование и правила для больших репозиториев. Дадим готовый конфиг и схему внедрения без «остановки разработки».
Содержание
Практическая настройка pre-commit для Python: линтеры, автоправки и контроль скорости сборки
Инструменты статического анализа и форматирования в Python сегодня стали почти стандартом: ruff заменяет целый зоопарк линтеров, а автоформатирование делает стиль воспроизводимым. Но на практике самая частая проблема звучит так: «Хук срабатывает, но сборка стала медленнее, и команда начала его отключать/обходить». Причина почти всегда одна — pre-commit настроен без учета производительности и без продуманной политики изменений в критичных файлах.
Ниже разберём, как собрать рабочий pre-commit-пайплайн для Python так, чтобы он действительно повышал качество кода и при этом не превращался в узкое горлышко. Фокус: ruff (линтер+форматтер), запрет правок тестов/снапшотов, кэширование и правила для больших репозиториев. В конце — готовый конфиг и схема внедрения без остановки разработки.
Что важно понять перед настройкой
pre-commit — это не «ещё один линтер», а оркестратор проверок при каждом коммите. Он умеет:
- запускать набор хуков до записи коммит-объекта;
- использовать виртуальные окружения для изоляции;
- кэшировать результаты (в зависимости от хуков);
- запускать хуки по файлам (и тем самым экономить время).
Качество настройки обычно определяется тремя параметрами:
- Стабильность: хуки должны быть детерминированными и давать одинаковые результаты в разных средах.
- Политика изменений: где автофиксы допустимы, а где — нет.
- Производительность: хуки должны быстро проходить по типичным коммитам и не ломать интерактивный workflow.
Чтобы добиться этого, нужно понимать, как именно pre-commit вызывает инструменты и как ruff ведёт себя при форматировании и автопочинке.
Архитектура пайплайна: что и где запускать
Для ruff обычно хочется три слоя:
- проверка стиля/формата (как правило, через
ruff format --check); - линтер (через
ruff check); - автоправки (опционально, через
ruff check --fixи/или форматирование без--check).
При этом важно различать:
- что можно автоисправлять без участия разработчика;
- что лучше запретить автоизменением (например, тесты, где изменение ожиданий может скрыть проблему).
Почему тесты — особая зона
Даже если у вас «только стиль не совпал», автофиксы могут затронуть строки в тестах (особенно если форматирование меняет переносы строк, отступы, конструкции с assert). На уровне результата это может казаться безобидным, но в реальной разработке тесты — это документация поведения. Любая массовая правка тестов:
- затрудняет ревью (diff становится шумным);
- усложняет поддержку «почему именно сломалось»;
- создаёт риск скрытых изменений ожиданий.
Поэтому разумная политика: в тестах разрешить только проверки, а автопочинки — только для исходного кода.
Подготовка: структура репозитория и baseline
Перед внедрением полезно понять, как устроены файлы:
- исходники:
src/или пакет в корне; - тесты:
tests/; - конфиги:
pyproject.toml; - большие каталоги (в которых форматирование не должно выполняться):
generated/,migrations/,vendor/, и т.п.
Если тесты действительно выделены в tests/, можно задать правило: хуки, которые меняют файлы, запускаются только на исходниках, а хуки проверки — на всё.
Минимальный набор конфигурации для ruff
pre-commit сам по себе не знает ваших правил. Поэтому базовые настройки должны лежать в pyproject.toml. Ниже пример фрагмента (не претендующий на «лучшие настройки для всех», а на то, чтобы было от чего отталкиваться):
# pyproject.toml
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM", "PL"]
ignore = ["E501"] # если используете format, часто E501 не нужен
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
Если у вас уже есть pyproject.toml — не переписывайте его, используйте текущий. Ключевое: ruff должен быть настроен единообразно, чтобы --check и --fix/format не вели себя неожиданно.
Готовый конфиг pre-commit для Python и ruff
Ниже — практичный /.pre-commit-config.yaml, который решает задачи статьи:
- линтер
ruff check; - форматтер
ruff format; - автоправки только для исходников (а тесты — только проверяем);
- контроль скорости: фильтрация файлов, лимиты, и рекомендации по кэшированию.
Важно: ниже предполагается, что тесты лежат в
tests/. Если у вас другой путь — замените regex.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
hooks:
# 1) Форматирование: проверка на всём, где разрешено форматировать
- id: ruff-format
name: ruff format (check)
args: ["--check"]
# Запускаем на Python-файлы, кроме типичных "не трогаем"
exclude: ^(generated/|migrations/|vendor/|\.venv/|\.tox/)
types_or: [python]
# 2) Линтер: проверка на всём
- id: ruff
name: ruff check
exclude: ^(generated/|migrations/|vendor/|\.venv/|\.tox/)
types_or: [python]
# 3) Автоправки линтера: только для исходников
- id: ruff
name: ruff check (fix - src only)
args: ["--fix"]
# Не трогаем тесты и прочие зоны
exclude: ^(tests/|generated/|migrations/|vendor/|\.venv/|\.tox/)
types_or: [python]
# 4) Форматирование после автоправок: только для исходников
- id: ruff-format
name: ruff format (write - src only)
# Без --check — инструмент будет менять файлы
exclude: ^(tests/|generated/|migrations/|vendor/|\.venv/|\.tox/)
types_or: [python]
# Дополнительно: можно добавить проверки конфигов/текстов,
# но для задачи статьи оставим фокус на Python+ruff.
# Общие настройки pre-commit
default_stages: [commit]
fail_fast: false
Что здесь происходит на практике
ruff format (check)запускается на всех*.py, кроме исключённых директорий. Это защищает от «почти форматирования» — коммит не пройдёт, если формат не соответствует правилам.ruff checkтоже проверяет всё. Так вы фиксируете и стиль, и потенциальные ошибки.ruff check (fix - src only)— автопочинка только для исходников. Тесты не будут меняться автоматически.ruff format (write - src only)гарантирует, что после автоправок исходники окажутся в правильном формате.
Это комбинация, которая обычно хорошо работает в командах: разработчик получает автофиксы там, где это безопасно, и явно отвечает за изменения тестов.
Контроль скорости: кэширование, ограничение объёма и выбор стратегии
1) Кэш pre-commit: где выигрыш и как не сломать
pre-commit хранит виртуальные окружения и результаты в каталоге кэша. Кэш ускоряет повторные запуски и переиспользует зависимости.
Проверьте, что у команды не настроены конфликтующие переменные окружения. В типичной среде можно оставить настройки по умолчанию.
На CI обычно кэширование делают отдельно, но даже локально полезно понимать механику:
- если вы обновляете
revхуков, кэш частично инвалидируется; - если меняется
pyproject.toml, часть проверок всё равно будет пересчитываться (но pre-commit всё равно управляет окружением).
2) Фильтрация файлов — самый недооценённый рычаг
В большом репозитории главная причина замедления — хуки запускаются на слишком много файлов. pre-commit и так фильтрует по staged-файлам, но:
- некоторые команды коммитят «всё подряд»;
- иногда staged включает массовые изменения;
- часть файлов может попадать в check по ошибочному паттерну.
Поэтому в конфиге выше есть exclude, который вырезает явные «не трогаем» зоны. Это экономит секунды на каждом коммите.
3) Два режима: проверка в коммите и форматирование отдельно
Для сверхбыстрого workflow иногда делают иначе:
- в коммит — только
ruff checkиruff format --check; - форматирование/фиксы — по требованию: отдельной командой
pre-commit run --all-filesилиpre-commit run ruff check (fix).
Однако в нашем конфиге автофиксы включены только для исходников — это компромисс: качество растёт, а время не расползается.
4) Не злоупотребляйте «--fix» на всё подряд
--fix потенциально может:
- менять импорт-структуру;
- делать более широкие замены, чем ожидает автор коммита;
- создавать “diff noise” в файлах, к которым вы не стремились.
Потому запрет на фиксы в tests/ — не просто удобство, а инструмент контроля диффа и скорости ревью.
Как запретить изменения тестов на уровне пайплайна
Мы уже ограничили автоправки через exclude для тестов. Но есть ещё один момент: даже если хуки не трогают тесты, форматирование иногда может затронуть вложенные файлы при неправильных паттернах.
Чтобы минимизировать риск:
- убедитесь, что
exclude: ^(tests/...)совпадает с реальной структурой путей; - учитывайте, что pre-commit видит пути относительно корня репозитория;
- если у вас тесты в
app/testsилиbackend/tests, поменяйте regex соответствующе.
Дополнительно можно усилить контроль на уровне процесса: командой ревью проверить, что коммит с тестами действительно менялся автором вручную.
Схема внедрения без остановки разработки
Самая неприятная ситуация — когда вы добавляете хук в середине проекта и внезапно обнаруживаете, что в репозитории уже много нарушений. Тогда каждый коммит превращается в борьбу с накопленным техдолгом.
Правильная схема внедрения обычно выглядит так:
Шаг 1. Ввести пайплайн в режиме «только проверка» (на этапе теста)
Начните с конфигурации, где автоправки выключены, а проверки включены. Это можно сделать, временно оставив только ruff-format --check и ruff check.
Затем прогоните локально:
pre-commit run --all-files
Получите список файлов, где уже есть нарушения. Зафиксируйте масштаб.
Шаг 2. Сделать один «технический» коммит на приведение к стандартам
Дальше действуйте одним из вариантов:
- либо прогнать
ruff check --fixиruff formatвручную по репозиторию; - либо временно включить автоправки в конфиге и запустить
pre-commit run --all-files.
В большинстве команд предпочтительнее именно один техкоммит: он делает историю чище, а не размазывает правки по десяткам PR.
Шаг 3. Включить автоправки только на безопасных путях
После того как baseline выровнен, можно включать --fix и форматирование без --check для исходников (как в финальном конфиге выше). Это снизит сопротивление внедрению: команда видит, что хук помогает, а не ломает.
Шаг 4. Настроить правило “не пускать тесты в автофикс”
Именно здесь становится критичным исключение tests/ из «пишущих» хуков. Команда получает предсказуемый дифф: тесты меняются только по намерению автора коммита.
Как настроить pre-commit для больших репозиториев: практические рекомендации
В больших кодовых базах проблема обычно не в том, что ruff медленный. Проблема — в том, что “hooks per commit” начинают быть слишком частыми и слишком тяжёлыми.
Ниже набор решений, которые реально работают:
1) Явно исключайте авто-генерируемое
Каталоги вроде generated/, migrations/, vendor/ почти всегда не должны проходить форматирование и линтинг. Иначе вы платите временем за файлы, которые всё равно не редактируются вручную.
2) Контролируйте, какие файлы попадают в хук
Если в репозитории есть специфические расширения (например, .pyi, .pyw), задайте types_or или дополнительные паттерны. Для стандартных пайплайнов достаточно types_or: [python].
3) Разделяйте «быстрое» и «глубокое»
В некоторых проектах делают два уровня:
- быстрые проверки в
commit; - глубокие проверки (интеграционные, с
--all-files) по расписанию, например nightly.
Для ruff обычно достаточно commit-level, но если вы добавляете дополнительные хуки (например, типа mypy/тяжёлых тестов), это становится критичным.
4) Согласуйте версию ruff на всех машинах
В командах часто бывает так: разработчики обновили ruff локально, но pre-commit использует закрепленную версию. В итоге возникают расхождения: “у меня проходит”. Закрепляйте версию через rev, как в примере.
Базовые команды для диагностики
Когда что-то пошло не так, важно быстро локализовать проблему. Вот набор команд:
Запуск по staged-файлам (обычный сценарий)
pre-commit run
Запуск по всем файлам (для baseline и техдолга)
pre-commit run --all-files
Принудительно выполнить один хук (например, формат или fix)
pre-commit run ruff-format
pre-commit run ruff
Если у вас несколько хуков с одинаковым id, но разными именами, используйте --hook-stage или явно по имени хука (если в вашей версии pre-commit это удобно). Часто проще смотреть список хуков:
pre-commit run --list
Типичные ошибки и как их избежать
Ошибка 1: включить --fix для всех, включая тесты
Итог — массовый diff и шум в PR. Решение: исключайте tests/ (или другие чувствительные директории) из write-хуков.
Ошибка 2: забыть про ruff format --check
Если формат только «проверяется» нестрого или выключен, линтер может начинать спорить с автоформатом. В итоге команда получает частые “почему всё не совпадает”. Решение: ruff-format --check должен быть частью пайплайна.
Ошибка 3: разные версии ruff в локальном и pre-commit окружении
Если вы запускаете ruff руками другой версии, а pre-commit — другой, различия неизбежны. Закрепляйте версию в rev.
Ошибка 4: слишком широкие хуки (нет исключений)
В большом репозитории это превращается в постоянные потери времени. Начинайте с честного exclude-листа: сгенерированное, vendor, мёртвые директории.
Как это выглядит в workflow команды
Практическая модель, которую обычно выбирают команды после внедрения:
- Разработчик вносит изменения.
- При коммите pre-commit автоматически чинит исходники (
ruff check --fixи затем форматирование исходников). - Тесты проверяются, но не меняются автоматически.
- Если проверка формата или линтера падает — разработчик получает понятный diff и исправляет вручную (или запускает
pre-commit run).
Это снижает количество “пустых” PR, ускоряет ревью и постепенно очищает кодовую базу.
Выводы
Хорошая настройка pre-commit для Python — это не список хуков, а система управляемых компромиссов между качеством, предсказуемостью диффа и скоростью. Для ruff-пайплайна особенно важно:
- иметь отдельные шаги проверки формата и линтинга;
- включать автоправки только там, где это безопасно (часто — для
src/, но не дляtests/); - контролировать скорость через исключение generated/vendor/migrations и аккуратную фильтрацию;
- внедрять поэтапно: сначала проверки, потом единый baseline, затем включение фиксов.
Если вам нужен дополнительный «вход в тему», чтобы уверенно понимать, какие правила ruff включать и как читать отчёты, полезно пройти материал вроде “ruff – для начинающих!” — но даже без него, опираясь на приведённый конфиг и схему внедрения, можно собрать устойчивый пайплайн самостоятельно.
Комментарии
Пока нет комментариев