Опирается на правила: PY-RUFF-1, PY-RUFF-2, PY-RUFF-3, PY-RUFF-4, PY-RUFF-X1 из Python Style Guide → раздел 9. Enforcement.

Важно знать

  • ruff обязателен на всех Python-сервисах; конфигурация — в pyproject.toml, не в разрозненных .flake8/.isort.cfg.
  • mypy --strict запускается в CI; ослабление — только per-module override с обоснованием в pyproject.toml.
  • ruff check + ruff format --check + mypy — на каждом CI-прогоне; любое нарушение — fail.
  • # noqa и # type: ignore допустимы только с кодом правила и justify-комментарием.
  • ruff покрывает механику: нейминг (PY-2.*), импорты (PY-3.*), формат (PY-5.*); в findings review эти нарушения не дублируются.
  • Семантику (PY-4.*, PY-6.*, PY-7.*, PY-8.*) ловит ucp-py-style-review, не инструменты.
  • Отключение правил «потому что мешают» без командного обсуждения — источник расхождения conventions между сервисами.

ruff заменяет flake8, isort, pep8, pyflakes и black одним инструментом. mypy --strict закрывает дыры в типах, которые ruff не видит. Вместе они формируют детерминированный барьер на CI: всё, что ловится механически, ловится автоматически — ревью остаётся для семантики.

Конфигурация в pyproject.toml

PY-RUFF-1: единственный допустимый источник конфигурации — pyproject.toml. Никаких .flake8, .isort.cfg, setup.cfg рядом.

[tool.ruff]
line-length = 120

[tool.ruff.lint]
select = [
    "E",
    "W",
    "F",
    "I",
    "N",
    "B",
    "UP",
    "S",
    "RUF",
]
ignore = []

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
line-ending = "lf"

[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_configs = true

line-length = 120 — override от дефолтного 88 (см. PY-5.2); team-дефолт 88 или 120 фиксируется однократно и применяется везде. Смешивать разные значения между сервисами — PY-RUFF-X1.

Запуск в CI

PY-RUFF-3: три проверки запускаются в CI как единый барьер.

# .github/workflows/ci.yml
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install ruff mypy
      - run: ruff check .
      - run: ruff format --check .
      - run: mypy .

ruff check — нарушения lint-правил. ruff format --check — форматирование без изменения файлов; при расхождении — non-zero exit. mypy — типовая проверка. Все три — отдельные шаги: при падении первого второй не запускается, что ускоряет обратную связь.

Локально перед коммитом:

ruff check . --fix
ruff format .
mypy .

--fix применяет автоисправления для правил, где это безопасно (исправление импортов, удаление unused, pyupgrade). Форматтер вносит изменения in-place.

Разделение зон ответственности

PY-RUFF-4: ruff — механика, ucp-py-style-review — семантика.

ЗонаИнструментПримеры
Нейминг (PY-2.*)ruff (набор N)snake_case модулей, PascalCase классов, UPPER_SNAKE_CASE констант
Импорты (PY-3.*)ruff (наборы F, I)порядок групп, unused, wildcard
Формат (PY-5.*)ruff formatотступы, длина строки, trailing comma
Код (PY-4.*)ucp-py-style-reviewguard clause, mutable default, exception без re-raise
Типы (PY-6.*)mypy, ucp-py-style-reviewX | None вместо Optional, Protocol, Decimal
Комментарии (PY-7.*)ucp-py-style-reviewinline-комментарии в коде, noqa без justify
Современные фичи (PY-8.*)ucp-py-style-reviewmatch/case, @dataclass(frozen=True, slots=True)

Findings review никогда не цитируют нарушения E/W/F/I/N — всё это уже поймал ruff до merge.

mypy --strict и per-module override

PY-RUFF-2: mypy --strict — базовая строгость. Флаги strict включают:

--disallow-untyped-defs
--disallow-incomplete-defs
--check-untyped-defs
--disallow-any-generics
--warn-redundant-casts
--warn-unused-ignores
--no-implicit-reexport

Сервис с новым кодом идёт сразу в strict = true. Если подключается существующая база без аннотаций — ослабление per-module с явным обоснованием:

[tool.mypy]
strict = true

[[tool.mypy.overrides]]
module = ["payment_gateway.*", "third_party_client.*"]
ignore_missing_imports = true
disallow_untyped_defs = false

Комментарий в pyproject.toml объясняет причину: «third_party_client не имеет stubs, обновить после добавления py.typed». Без обоснования — нарушение PY-RUFF-2.

noqa и type: ignore

PY-RUFF-3, PY-7.X2: # noqa и # type: ignore допустимы только с кодом и justify.

result = external_sdk.call()  # type: ignore[no-any-return]  # SDK не имеет py.typed, стабов нет — #123

from sber_notify_client import helper  # noqa: F401  # используется в __init__.py через re-export

Запрещённые формы:

result = external_sdk.call()  # type: ignore
from sber_notify_client import helper  # noqa

Без кода правила подавление неотличимо от «я не разобрался» — ревьюер не может оценить, правомерно ли оно. warn_unused_ignores = true в mypy автоматически снимает устаревшие # type: ignore, когда тип исправлен.

Запрет разрозненных конфигов

PY-RUFF-X1: отключение правил без командного обсуждения — расхождение conventions.

[tool.ruff.lint]
ignore = [
    "E501",
    "N806",
]

Если правило мешает на конкретном файле по объективной причине (автогенерация, proto-output, seed-данные) — # noqa: <code> с justify на строке или per-file-ignores:

[tool.ruff.lint.per-file-ignores]
"migrations/**" = ["N806", "E501"]
"tests/fixtures/**" = ["S101"]

Разрозненные .flake8 или setup.cfg рядом с pyproject.toml — удалить; ruff читает только pyproject.toml.

Что запрещено

АнтипаттернПравилоЧто взамен
.flake8, .isort.cfg, setup.cfg для lint/formatPY-RUFF-1[tool.ruff] в pyproject.toml
mypy без strict = true в новом сервисеPY-RUFF-2strict = true; ослабление — per-module с justify
ruff check без mypy в CIPY-RUFF-3оба инструмента в каждом прогоне
# type: ignore без кода и комментарияPY-RUFF-3, PY-7.X2# type: ignore[no-any-return] # SDK без stubs — #issue
# noqa без кода правилаPY-RUFF-3, PY-7.X2# noqa: F401 # justify: ...
ignore = ["N806"] без обоснования в pyproject.tomlPY-RUFF-X1обсуждение в команде + комментарий
line-length разный в разных сервисахPY-RUFF-X1единый дефолт (88 или 120) через shared config

Куда дальше

  • Форматирование — длина строки, перенос, горизонтальное выравнивание (PY-5.*).
  • Именование — что ловит ruff набором N, а что остаётся на review (PY-2.*).
  • Выражения — guard clause, mutable default, comprehension vs. цикл (PY-4.*).
  • Раздел «SAST по коду» — ruff S-правила как часть security-барьера.