ruff на максималках: правила, исключения и единый стиль команды
Как договориться о правилах, настроить конфиг под реальный код-стайл команды и не спорить о форматировании и линтинге бесконечно.
Содержание
ruff на максималках: правила, исключения и единый стиль команды
Договориться о код-стайле в команде — это всегда про нечто большее, чем “нравится/не нравится”. В реальности споры обычно крутятся вокруг форматирования, переносов, порядка импортов, именования, и особенно вокруг того, кто и как должен “править линтером” — вручную или автоматически, и почему линтер ругается именно на этот кейс.
ruff — один из самых практичных инструментов для наведения порядка в Python-проектах: он быстрый, покрывает много правил, поддерживает конфигурацию исключений и позволяет выстроить единый стиль. Но “поставить ruff и забыть” не работает: без продуманной конфигурации и правил изменений вы получите либо бессмысленный шум, либо бесконечные обсуждения на тему “линтер неправ”.
Эта статья — про то, как договориться о правилах, настроить конфиг под реальный код-стайл команды и минимизировать споры. Мы разберём практические подходы, типовые ошибки и рабочие примеры конфигурации. По ходу я аккуратно упомяну курс ruff – для начинающих! — как один из способов быстро подтянуть базу.
Что именно делает ruff (и почему споры неизбежны без рамок)
ruff — это статический анализатор, который объединяет множество правил из экосистемы линтинга: flake8-подобные проверки, pycodestyle/pyflakes, isort-логика, некоторые правила из других источников и собственные проверки. На практике ruff решает две задачи:
- Правила стиля/качества (например, “не используй
import *”, “не дублируй импорт”, “не допускай неиспользуемые переменные”). - Авто-форматирование/авто-исправления (часть правил умеет править код через
ruff check --fixилиruff format— в зависимости от типа проблемы и настроек).
Когда команда не согласовала, что именно считать ошибкой, предупреждением и чем допустимо пренебречь ради “наследия” — начинаются споры. Часть участников считает всё, что ругается, “ошибка разработки”. Другая часть — “пусть линтер подстроится под проект”.
Поэтому основная идея — не искать “идеальные правила”, а построить систему договорённостей, где:
- правила имеют статус (ошибка/предупреждение);
- часть правил включается “с нуля”, часть — только на новый код;
- есть механизм исключений без злоупотреблений;
- есть воспроизводимость: одинаковая конфигурация и одинаковые версии на всех машинах/CI.
Стратегия внедрения: “сейчас всё исправим” или “не тормозим разработку”
Первый практический выбор — как заходить в проект.
Подход A: жёсткий старт (всё привести в порядок сразу)
Плюсы: меньше долгосрочного техдолга, проще поддерживать единый стиль.
Минусы: может потребоваться большой PR, сложнее провести релиз без пауз.
Этот подход оправдан, когда:
- кодовая база относительно свежая;
- команда может выделить время на “купол качества” (например, один спринт);
- правило уже не вызывает концептуальных разногласий.
Подход B: постепенное внедрение (новый код — строго, старый — по остаточному принципу)
Плюсы: минимальные изменения в текущей кодовой базе, меньше конфликтов.
Минусы: на протяжении времени есть “двойной стандарт”: новый код под линтер, старый — с послаблениями.
Для большинства команд это реалистичнее. Ключевой механизм — таргетить анализ на конкретный набор путей/файлов или использовать режимы исключений и фильтрации по “типу нарушения”.
Структура конфигурации: где ruff хранит правила
ruff поддерживает конфигурацию через pyproject.toml (чаще всего) или отдельные файлы. Практика для команды — хранить в pyproject.toml, чтобы не разрывать “источник правды”.
Обычно удобно разделить:
- какие правила включены;
- какие правила исключены;
- какие пути игнорируются;
- какие стилистические настройки применяются (например, стиль импортов).
Ниже — пример каркаса, который потом можно расширять:
# pyproject.toml
[tool.ruff]
target-version = "py311"
line-length = 100
# По умолчанию ruff будет анализировать исходники в проекте
# (конкретный набор можно уточнять через include/exclude).
exclude = [
".git",
".venv",
"build",
"dist",
]
[tool.ruff.lint]
# Выберите режимы: правила и severity.
# Ниже покажем подход “набор правил + исключения”.
Правила и “единый стиль”: как договориться без бесконечных обсуждений
Теперь — самый важный блок. Договорённость не должна быть “в голове”. Её нужно превратить в конфигурацию и правила работы команды.
1) Определите “уровни строгости”: что считается нарушением процесса
Частая ошибка — включить максимум правил и оставить всем свободу трактовки. Итог: линтер начинает конфликтовать с реальным темпом разработки.
Вместо этого стоит договориться о матрице:
- Ошибки (must-fix): правила, нарушающие корректность или явно указывающие на проблемы (например, неиспользуемые переменные, неправильные импорты, потенциальные ошибки).
- Предупреждения (should-fix): правила стиля/качества, которые полезны, но не должны стопорить каждую правку.
- Игнор/исключения: то, что невозможно/дорого исправить прямо сейчас или что конфликтует с архитектурными решениями проекта.
ruff позволяет управлять тем, какие правила включены, и настраивать наборы ошибок через селекты/айклюды (в зависимости от версии ruff и типа правил). На практике часто делают так: сначала включают популярные группы, затем точечно отключают спорные.
Например, для стартового “разумного максимума” команда может включить:
- базовые проверки из flake8/pyflakes-подобных (качество/ошибки);
- isort-проверки (порядок импортов);
- часть pycodestyle (синтаксический стиль).
2) Не смешивайте “правки стиля” и “правки логики” в одной философии PR
Если ruff используется и как линтер, и как авто-исправлятор, команда должна договориться, когда это применяется.
Реалистичная схема:
- PRы по функциональности не должны содержать случайные исправления “форматирования”.
- если форматирование нужно привести — это отдельный “стилизационный” PR или автоматический шаг в CI/pre-commit, который применяется прозрачно.
Пакетное применение ruff check --fix может генерировать большие диффы. Поэтому лучше запускать форматирование/исправления предсказуемо (например, pre-commit) и отслеживать, что именно делает автофиксер.
3) Настройте line length и импортный стиль так, как принято в команде
Две причины, почему люди спорят с линтером чаще всего:
- длина строки и переносы;
- порядок импортов.
Обе вещи легко зафиксировать.
Например, если команда хочет line-length = 100 и конкретную логику сортировки импортов, это должно жить в конфиге.
[tool.ruff]
line-length = 100
[tool.ruff.lint.isort]
# Пример: аккуратно настроить формат
force-wrap-aliases = true
known-first-party = ["your_package"]
known-third-party = ["requests", "numpy"]
Если у вас “и так сортируется как-то” — на практике это приводит к тому, что разные разработчики получают разные диффы. Конфиг должен быть общим.
Исключения: как разрешить конфликты и не превратить их в дыру
Исключения неизбежны: legacy-код, внешние API, генерация кода, специфические паттерны. Важно одно: исключения должны быть управляемыми, ограниченными и объяснимыми.
Варианты исключений
- Игнорировать правила для конкретных файлов/путей в конфиге.
- Игнорировать конкретные правила в конкретных местах через
# noqa: CODE. - Использовать допустимые формы
noqaи договориться, когда это разрешено.
1) Исключения на уровне путей: для legacy и generated
Например, если у вас есть автогенерируемые модули:
[tool.ruff]
exclude = ["generated/**", ".venv", "build", "dist"]
или более точечно:
[tool.ruff.lint]
per-file-ignores = {
"generated/**/*.py" = ["ALL"],
"tests/test_*.py" = ["SOME_RULE_ID"]
}
Подводный камень: per-file-ignores легко превратить в “мы здесь ничего не исправляем”. Поэтому стоит договориться: исключения на тестах — допустимы, но только для конкретных правил и с понятным обоснованием.
2) Исключения через noqa: только точечно и с указанием кода
Наиболее полезная практика — никогда не делать # noqa без кода, если в конфиге много правил. Тогда вы теряете контроль.
Пример:
from typing import Any
def f(x: Any) -> Any:
# noqa: ARG001 (аргумент намеренно не используется)
return x
Если вы написали # noqa без кода, то ruff “молча проедает” проблему. В команде это часто становится нормой, и качество деградирует незаметно.
3) “Временные” исключения должны иметь срок
Полезная договорённость: если правило отключено из-за миграции или дефицита времени — это должно быть помечено как временное (например, комментом рядом с исключением) и привязано к задаче/тикету. Ruff сам по себе не ведёт историю “временности”, но команда может.
Как включать правила “с умом”: минимальный шум и максимальная пользу
Рассмотрим практическую схему набора правил.
1) Начните с набора, который почти всем полезен
Обычно это:
- устранение неиспользуемых импортов и переменных;
- базовые ошибки в коде (аналог pyflakes);
- изоляция потенциальных проблем;
- изорт (упорядочивание импортов).
2) Затем добавляйте “спорные” правила после того, как люди согласовали формат
Правила стиля чаще всего конфликтуют с личными привычками. Поэтому лучше:
- сначала договориться о line length;
- потом — о размещении импортов;
- потом — о форматировании (включая правила, которые влияют на автоправки).
Иначе вы получаете эффект: линтер выглядит “непонятным”, потому что он ведёт к диффам по форматированию, которые разработчик не контролирует.
3) Управляйте прогрессом через CI и pre-commit
Один из лучших способов прекратить споры — сделать процесс механическим.
- разработчик запускает ruff локально (предсказуемо);
- CI прогоняет тот же набор правил;
- коммит не проходит, если есть нарушения “must-fix”.
Автоисправления и форматирование: как избежать “линтер сломал стиль”
ruff имеет возможности автоисправления, но важно понимать, что:
- не все правила исправляются автоматически;
- некоторые правки влияют на диффы больших файлов;
- автофиксер может быть не тем, что “ожидает” команда, если конфиг не согласован.
Практика: включайте авто-фикс только для того, что действительно согласовано
Обычно разумный путь:
- сортировку импортов и простые правки стиля автофиксить можно;
- сложные правки оставлять на стадию “ручной ревью” — особенно если ruff меняет поведение или структуру.
Пример: как запускать ruff в режиме проверки и автофикса
# Проверка
ruff check .
# Автофикс (только те правила, для которых есть fix)
ruff check . --fix
# Если вы используете отдельный форматтер ruff (зависит от версии/конфигурации),
# можно запускать ruff format:
ruff format .
В команде стоит договориться: каким образом правки попадают в PR. Например:
- “Мы всегда запускаем ruff check --fix перед коммитом”;
- или “CI всегда форматирует и запрашивает повторный прогон” (но это может быть неудобно).
На практике чаще всего используют pre-commit, чтобы изменения были видны заранее.
pre-commit: единый процесс вместо бесконечной переписки
Чтобы “один стиль на команду” работал, нужно синхронизировать локальные шаги.
Схема такая:
- в репозиторий добавляется
.pre-commit-config.yaml; - в pre-commit настраиваются хуки ruff (check и fix, по ситуации);
- разработчики получают одинаковое поведение локально;
- CI совпадает с локальными правилами (или имеет тот же набор).
Пример (упрощённый):
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
hooks:
- id: ruff
args: [--fix]
Подводные камни:
- версия ruff в pre-commit должна быть согласована с версией в CI;
- аргументы (например,
--fix) должны соответствовать договорённости команды; - если вы не хотите, чтобы pre-commit менял код, уберите
--fixи используйте только проверку.
Типовые ошибки конфигурации (которые почти всегда ломают процесс)
Ошибка 1: Нет “источника правды” по конфигу
Если ruff смотрит на разные конфиги в разных местах (или у части разработчиков старый pyproject.toml/не тот каталог), вы получите “у меня не ругается”.
Решение:
- один конфиг в репозитории;
- фиксируйте версию ruff (в lock-файлах или в CI/pre-commit).
Ошибка 2: Слишком широкие per-file-ignores
Когда исключения начинаются как “временно”, они быстро разрастаются. В итоге ruff превращается в “проверяет только половину проекта”.
Правильный подход:
- точечные правила;
- “ALL” только для generated или строго контролируемых кейсов;
- регулярная ревизия исключений.
Ошибка 3: noqa без кода и без объяснения
Это подрывает смысл статического анализа.
Правильный подход:
- указывать код правила;
- избегать “заглушек” на большие блоки.
Ошибка 4: Конфликт line-length / переносов с форматтером
Если команда использует несколько инструментов, которые по-разному определяют правила форматирования, будут вечные диффы.
Один из принципов: выберите один инструмент как “финальный” для форматирования (или чётко разделите ответственность: например, ruff исправляет импорты и простые вещи, а форматтер — выравнивание/переносы).
Практический рецепт: как зафиксировать правила в команде
Ниже — рабочий “скелет” процесса, который помогает почти всегда.
Шаг 1: Принять базовый стиль и зафиксировать параметры
line-length- стиль импортов (
known-first-party, сортировка) - минимальный набор правил (на качество/ошибки и самые полезные стилистические)
Шаг 2: Решить, что делать со старым кодом
- либо “всё сразу”
- либо “строго на новый код” + план по миграции (через тикеты и итерации)
Если вы не готовы к миграции, хотя бы убедитесь, что линтер не тормозит разработку: исключите legacy-части на уровне путей.
Шаг 3: Установить правило для исключений
noqaтолько с кодом- исключения на файлы только с конкретными правилами
- запрет на “глушение” целых направлений без причины
Шаг 4: Автоматизировать локальный процесс и CI
- pre-commit проверяет одинаково
- CI повторяет те же проверки
- разработчики не обсуждают форматирование руками — обсуждают только смысл и корректность исключений
Шаг 5: Проводить ежемесячную ревизию качества конфигурации
Раз в месяц “подчищайте”:
- исключения, которые уже не нужны;
- правила, которые больше не вызывают споров (можно включать сильнее);
- места, где люди продолжают добавлять
noqa— возможно, там проблема конфигурирования или рефакторинг стоит сделать один раз.
Вывод: ruff становится “контрактом”, когда правила оформлены в конфиг и процесс
ruff можно настроить так, что он будет не предметом споров, а механическим контрактом между разработчиками: что считается нарушением, как именно это измеряется и где допустимы исключения.
Ключ — не пытаться сразу “включить всё на максимум” без договорённостей, а построить:
- понятный набор правил (и уровни строгости),
- жёсткую фиксацию параметров стиля,
- контролируемые исключения,
- одинаковый запуск локально и в CI.
Если у команды в целом пока мало практики с настройкой
Комментарии
Пока нет комментариев