Ruff и форматирование: как добиться “нулевых” стычек в команде и ускорить PR
Поговорим о стратегии внедрения ruff и автоформатирования: единый конфиг, правила для разных веток, pre-commit/CI и как договориться о форматах без долгих споров. В результате получите понятный процесс, который делает ревью быстрее и безопаснее.
Содержание
Ruff и форматирование: как добиться “нулевых” стычек в команде и ускорить PR
Форматирование кода — одна из тех тем, где страсти в командах вспыхивают быстрее, чем компилятор успевает выдать первую ошибку. Сегодня кто-то пишет, что “это же читаемость”, завтра кто-то отвечает “это же стиль проекта”, а послезавтра выходит третья ветка, где правила “как-нибудь согласуем”. В итоге ревью превращается в обсуждение отступов и переноса строк вместо обсуждения архитектуры, корректности и качества тестов.
Хорошая новость: большую часть конфликтов можно устранить технически. В экосистеме Python эту задачу последовательно закрывают форматтеры и линтеры, и в последние годы заметно усиливается инструмент Ruff. Он одновременно действует как быстрый статический анализатор (линтер) и как инструмент автоформатирования (через механизмы, связанные с форматтером). Но чтобы добиться “нулевых” стычек в команде, недостаточно “просто включить Ruff”. Нужна стратегия внедрения: единый конфиг, правила для разных веток, интеграция в pre-commit и/или CI, а также организационная договорённость, которая превращает форматирование в предсказуемый процесс.
Ниже — практическое руководство по тому, как выстроить такой процесс, какие решения принять заранее и где чаще всего ломаются команды.
Почему конфликты в PR возникают даже при наличии линтеров
Конфликт обычно не из воздуха. Он рождается из несовпадения трёх вещей:
- Версия инструментов (Ruff/форматтер может обновляться).
- Правила (какие проверки включены и как именно они трактуются).
- Момент применения (кто и когда приводит код к виду: локально, в
pre-commit, в CI, при мердже).
Если хоть один из пунктов “плывёт”, ревьюер будет видеть расхождения, которые автор не мог (или не успел) устранить. В результате возникают “мелкие” комментарии, которые на дистанции превращаются в затяжные PR.
Ruff решает часть проблем напрямую: он быстрый, поддерживает настраиваемые правила и умеет действовать предсказуемо. Но чтобы он работал именно как “инструмент для компромисса”, а не как очередной источник споров, нужно правильно выбрать контур внедрения.
Принцип “один источник истины”: единый конфиг для команды
Единый файл конфигурации и контроль через репозиторий
Стандартная практика — хранить настройки Ruff в репозитории: например, в pyproject.toml. Это делает правила неизменными между участниками и окружениями.
Типовая структура секций:
- настройки линтера (какие правила включаем/выключаем);
- настройки выбора кодов ошибок/предупреждений;
- настройки форматирования (если вы используете режимы, связанные с автоформатированием);
- настройки исключений (ignore, per-file-ignores, exclude).
Пример “скелета” конфигурации:
[tool.ruff]
target-version = "py312"
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I"] # пример: ошибки/провалы + импорты
ignore = []
[tool.ruff.lint.isort]
known-first-party = ["your_package"]
Ключевая идея: конфиг — в git, и “правила форматирования” не обсуждаются каждый раз в комментариях. Они просто применяются.
Почему “разные конфиги под разные ветки” опасны
Иногда команды хотят ускорить работу: например, в main строгие правила, а в develop — мягче. Если делать это неаккуратно, вы получите эффект “код проходит в одной ветке, но падает в другой”. Это не только тормозит, но и формирует неприятный режим “гоняем PR в зависимости от статуса ветки”.
Решение — не плодить разные правила без необходимости, а договориться о режиме применения: например, вы допускаете временно предупреждения в отдельных ветках, но в “целевой” ветке они не должны попадать. Логику можно выразить через:
- различие “где считается качество” (например, только в CI для
main); - использование разных команд Ruff в CI (с разными параметрами);
- постепенный переход (deprecations/allowed fixes).
Главный принцип: внутри одной ветки правила должны быть предсказуемыми.
Настройка Ruff под форматирование: что именно вы хотите автоматизировать
Важно уточнить: когда команда говорит “форматирование”, она обычно имеет в виду разные вещи:
- Локальные правки, которые не требуют раздумий: перенос строк, пробелы, кавычки, порядок импорта.
- Стиль, который близок к “линту”: запреты на определённые конструкции, ограничения по длине строк и т.д.
- Рефакторинг по стилю: например, перестроение импорта или приведение к определённым шаблонам.
Ruff полезен именно на стыке (1) и (2). Для (1) вы используете автоформатирование, для (2) — линт. А конфликт возникает, когда команда не понимает, что именно делает каждый инструмент.
Практическая стратегия такая:
- Используйте Ruff как первую линию: он проверяет правила и может приводить код к формату (в зависимости от ваших настроек/режимов).
- Закройте “неформатируемое” обсуждением в PR там, где действительно есть содержание: имена, структура, корректность.
- Для импорта обычно достаточно ruff+isort-подсистемы, но нужно правильно указать “first-party” и “known” пакеты, иначе будут постоянные перестановки.
Локальная автоматизация: pre-commit как минимально болезненный вход
Почему pre-commit — лучший “первый шаг” для команды
CI — это хорошо, но CI всегда позднее: он отрабатывает после того, как код уже прошёл через вашу локальную итерацию. pre-commit позволяет сдвинуть большую часть “форматных” правок в момент коммита.
Это принципиально снижает число итераций PR:
- автор сам приводит код к стандарту;
- ревьюер видит согласованную форму;
- ревью фокусируется на смысле.
Конфиг .pre-commit-config.yaml
Пример конфигурации, где Ruff применяется и на форматирование (если поддерживается вашим сценарием), и на линт. В зависимости от версии Ruff и ваших целей могут отличаться команды. Ниже базовый, распространённый подход:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.4
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
Если ваш проект использует только линт без “форматного” хука, тогда оставляйте один ruff. Если важно автоматизировать порядок импортов и формат, добавляйте ruff-format (или ваш форматтерный хук в рамках Ruff-экосистемы).
Подводные камни pre-commit
-
Разные версии у участников
pre-commitфиксирует версию черезrev. Это снижает дрейф правил. Обновление делайте контролируемо: например, раз в спринт и с регенерацией форматного результата. -
Непредсказуемость “fix”
Флаг--fixможет исправлять часть проблем автоматически. Это хорошо, но не должно приводить к “внезапным” изменениям, которые автор не ожидал. Команда должна понимать, что определённые категории правок будут внесены автоматически. -
Время выполнения
Ruff быстрый, но всё же проект может быть крупным. Дайтеpre-commitтолько то, что реально нужно на commit (например,ruffиruff-format), а более тяжёлые проверки перенесите в CI.
CI как гарантия: разные режимы для разных веток без хаоса
В чём смысл CI: не “обсуждать”, а “останавливать”
CI должен давать два эффекта:
- если код не соответствует стандартам целевой ветки — PR не проходит;
- форматирование должно считаться “не предметом спора”, а “условием входа”.
Разделение режимов: “strict” для main, “informational” для develop
Вместо раздельных конфигов можно применять разные команды или параметры.
Пример идеи:
- Для
main: Ruff в режиме, где нарушения формат/линта считаются ошибкой (например,--output-format=githubи ненулевой exit code). - Для
develop: те же правила, но допускается мягкое состояние (например, разрешить warnings или запускать только определённые группы проверок).
На практике это означает, что команды CI могут различаться:
# main
ruff check . --output-format=github
ruff format --check .
# develop (пример мягче — зависит от ваших требований)
ruff check . --output-format=github
ruff format --check .
Если вы используете ruff format как обязательный шаг, то “мягкость” скорее не про формат, а про часть линтинга (например, не все группы правил включены).
Важно: не допускайте несовместимости форматов
Самый частый провал внедрения: в одной ветке формат считается, а в другой — игнорируется. Затем авторы начинают приносить в PR изменения “в стиле той ветки”, и ревью превращается в поле боя. Если вы хотите “нулевые стычки”, автоформат должен быть обязательным хотя бы для ветки, куда мерджат PR.
Разумная цель: форматирование всегда проходит одинаково, отличаться может только глубина линтинга на ранних стадиях.
Типовая стратегия миграции: как не остановить разработку
Внедрение Ruff часто ломает процесс не из-за инструмента, а из-за объёма “технического долга” в текущем репозитории: линтер увидит много проблем и проект “покраснеет” сразу.
Есть несколько рабочих подходов:
Подход A: постепенная очистка через “окно” (new code only)
Идея: вы включаете правила так, чтобы они не ломали существующий код, но ловили проблемы в новых изменениях.
Технически это делается через:
- исключения старых файлов (например,
excludeилиper-file-ignores); - запуск Ruff только на изменённых файлах в CI (через diff);
- строгий режим на
main, но мягкий на проверках “полного дерева”.
Это снижает психологическое сопротивление: команда не обязана “переписать всё” за один день.
Подход B: “один большой форматный коммит” + затем строгий режим
Если вы готовы к одномоментному улучшению, вы:
- фиксируете версию Ruff;
- прогоняете форматирование и автофиксы на всём репозитории;
- делаете один коммит с изменениями стиля;
- включаете жёсткий CI.
Плюсы: быстрый возврат к нормальному режиму. Минусы: большой diff может усложнить навигацию по истории изменений. Хорошо работает, если команда хорошо понимает риски и готова к большому PR внутри репозитория (например, “style unification”).
Подход C: гибридный — формат всегда, линт постепенно
Компромисс, который часто выигрывает: автоформат и импортный порядок включаются жёстко сразу, а линт-группы расширяются постепенно.
Это даёт:
- моментальный эффект “меньше разногласий”;
- снижение шумов в ревью;
- управляемое снижение долгов.
Договорённость о правилах: как снизить споры до нуля
Технические решения уменьшают трения, но люди всё равно будут спорить, пока не договорятся о политике.
Формула “код, который не проходит — не обсуждается”
Рекомендованный принцип:
- если CI или
pre-commitне пропустили изменение — автор исправляет автоматически/локально; - ревьюер не обсуждает формат вручную, потому что формат уже “зафиксирован” инструментом.
Для команды это означает смену ментальной модели: формат — это не часть ревью как процесса принятия решений, а часть процесса “входа”.
Как оформить правила так, чтобы они были понятны
В репозитории обычно достаточно файла CONTRIBUTING.md (или раздела в README):
- команду установки dev-зависимостей;
- как запускать
pre-commit; - как запускать Ruff вручную (на случай “надо посмотреть перед коммитом”).
Пример формулировки (схематично):
- Перед коммитом запусти:
pre-commit run --all-files. - Линт и форматирование выполняются автоматически.
- Если CI ругается — применяй автофиксы Ruff.
Это не требует многословия, но снимает главную проблему: “а что от нас хотят?”.
Конкретные команды: как разработчикам быстро приводить код к стандарту
Ниже набор команд, которые обычно составляют “минимальный набор” для новичков в проекте.
Проверка линта
ruff check .
Автоисправления там, где это безопасно и настроено
ruff check . --fix
Проверка форматирования (без изменений)
ruff format --check .
Применить форматирование к файлам
ruff format .
Запуск на конкретных файлах (удобно для локальной итерации)
ruff check path/to/file.py
ruff format path/to/file.py
Важно: разработчики должны получать один и тот же результат, что уменьшает “почему у меня проходит, а у тебя нет?”.
Как избежать типичных ошибок при внедрении Ruff
Ошибка 1: несогласованная версия Ruff
Решение: фиксировать версию в pre-commit и явно указать её в tooling документации (или использовать lock-файл, если вы ставите Ruff через зависимости проекта).
Если вы используете Python tooling (например, poetry или pip-tools), убедитесь, что Ruff попадает в dev-зависимости и фиксируется версией.
Ошибка 2: слишком агрессивные правила с первого дня
Если включить все проверки сразу, команда упрётся в массу автокоммитов и начнёт “прятать” проблемы. Лучше начать с ядра правил:
- базовые ошибки (
E,F), - импорты (
I), - формат и упорядочивание,
- затем расширять покрытие.
Ошибка 3: “per-file-ignores” без стратегии
Локальные ignore — полезны для временных участков, но без дисциплины игнор превращается в “вечное исключение”. Хорошая практика:
- использовать per-file-ignores временно;
- вести список исключений и план их удаления;
- документировать причину (почему игнор и какой ожидаемый срок).
Ошибка 4: форматирование не обязательное в целевой ветке
Если формат не является обязательным условием прохождения CI для ветки мерджа — вы не устраните конфликт, вы лишь перенесёте его. Держите ruff format --check . как обязательную часть проверки для ветки, куда принимаете PR.
Настройка “разных веток” без раздвоения истины: рабочая модель
Практическая модель, которая обычно хорошо работает:
- Форматирование — едино для всех веток, где живут PR в будущее (например,
developиmain). Это устраняет “почему у меня отформатировано иначе”. - Линт — одинаковый по правилам, но различается по строгости:
- где-то вы можете показывать результаты как предупреждения/комментарии, а на
main— блокировать; - или включать не все группы правил на ранних стадиях.
- где-то вы можете показывать результаты как предупреждения/комментарии, а на
Технически это реализуется не через два разных конфига, а через разные команды в CI. Один источник истины остаётся единым: pyproject.toml. Меняется лишь момент “взять вето или нет”.
Ускорение PR: что именно меняется в процессе ревью
Когда формат “схлопнут” до автоматического, ревью действительно ускоряется. Но не магически — меняется конкретная статистика:
- Становится меньше комментариев уровня “перенеси строку / переставь импорт”.
- Меньше ситуаций, когда автор отвечает “да, я поправлю, но позже”, потому что позже — это уже “встроенная задача инструмента”.
- Меньше циклов “CI не проходит → локально исправь → снова запроси ревью”.
Параллельно вы получаете более безопасный процесс: автоправки выполняются детерминированно, а проверки повторяемы в CI.
Ruff и автоформат: как понять, что вы действительно достигли “нулевых” стычек
Есть простые метрики процесса:
-
Доля комментариев в PR по форматированию
Если это “заметная часть” — форматирование не обязательное или не согласованное. -
Количество итераций до merge
После внедренияpre-commitи CI обычно падает число кругов “исправь стиль — исправь стиль снова”. -
Частота фраз “у меня проходит”
Если они появляются — значит версии/правила несовместимы. Решение обычно: фиксировать версии и унифицировать конфиги. -
Объём “стилевых” диффов
Если диффы оказываются огромными на каждый PR — вы либо форматируете не так, либо инструмент применяется не в том месте (например, форматирование не делается до коммита).
Мини-курс как способ закрепить практику
Если вы только начинаете и хотите не разбираться с Ruff “по кусочкам” через справку и случайные заметки, полезной отправной точкой может стать курс “ruff – для начинающих!”. Его стоит воспринимать как структурированный способ собрать модель: какие проверки включать, как настроить базовый конфиг и как выстроить рабочий процесс под команду.
Вывод: форматирование как часть пайплайна, а не предмет дискуссий
Ruff и автоформатирование могут реально превратить ревью в процесс обсуждения качества, а не стиля. Но эффект появляется только при наличии стратегии:
- держите единый
pyproject.tomlкак источник истины; - автоматизируйте формат и линт на локальном уровне через
pre-commit; - в CI обеспечьте обязательность форматирования для ветки мерджа;
- используйте “разные режимы” на разных ветках не через раздвоение конфигов, а через различия команд/строгости;
- планируйте миграцию, чтобы не получить “красный репозиторий” на старте;
- фиксируйте версии инструментов, иначе вы не устраните конфликты, а просто сделаете их непредсказуемыми.
В результате команда получает понятный процесс, где автоформат выполняется детерминированно, а ревью быстрее и безопаснее. И главное — споры о переносах и отступах перестают быть “стоимостью входа” в разработку.
Комментарии
Пока нет комментариев