Опирается на правила: PY-5.1PY-5.3, PY-RUFF-1PY-RUFF-3 из Python Style Guide → раздел 5. Форматирование.

Важно знать

  • ruff format — единственный форматтер; руками не выравнивать.
  • Длина строки — единая по проекту: 88 (дефолт ruff) либо 120 (командный override); фиксируется в pyproject.toml.
  • Горизонтальное выравнивание присваиваний и словарей запрещено — шум в diff при переименовании.
  • Отступ — 4 пробела; табы запрещены.
  • Перенос по бинарному оператору — перед оператором (Knuth-style: +, and в начале новой строки).
  • Висячая запятая в многострочных литералах обязательна — ruff format ставит её автоматически.
  • pyproject.toml — единственное место конфигурации; не setup.cfg, не .flake8.
  • ruff check + ruff format --check + mypy — три прогона CI; fail при любом нарушении.

Форматирование не должно занимать времени на ревью. Один раз зафиксировали ruff format в pyproject.toml и CI — дальше формат всегда правильный, без споров «куда поставить запятую».

ruff format — не выравнивать вручную

PY-5.1: форматирование делает ruff format, не разработчик.

ruff format src/ tests/
ruff format --check src/ tests/

ruff format совместим с black: одинаковые правила расстановки кавычек (двойные), trailing comma, переноса длинных выражений. Переключаться между ними без изменений в коде не нужно — на проектах с историей black переход на ruff format прозрачен.

Ручные выравнивания после ruff format — нарушение PY-5.1: при следующем прогоне форматтер их откатит, порождая ложный diff.

Длина строки

PY-5.2: длина строки — единая в проекте и зафиксированная в pyproject.toml.

[tool.ruff]
line-length = 88

Дефолт ruff format88 символов (наследство black). Команды с длинными именами доменных объектов или несколькими дженериками часто выбирают 120 — это командный override, не «как получилось»:

[tool.ruff]
line-length = 120
def find_active_orders_by_customer(
    customer_id: UUID,
    status: OrderStatus,
    page: int = 0,
) -> list[Order]: ...

При превышении лимита рефакторим структуру, не подбираем ширину под существующий код:

  • Длинная сигнатура → возможно, метод делает слишком много или принимает слишком много параметров.
  • Длинный вызов → multi-line с висячей запятой.
  • Длинная цепочка .method() → разбить на промежуточные переменные.

Перенос длинных выражений

PEP 8 и ruff format определяют три устойчивых паттерна.

Многострочный вызов с висячей запятой

order = Order(
    customer_id=customer_id,
    items=items,
    status=OrderStatus.PENDING,
    created_at=datetime.now(UTC),
)

Открывающая скобка — на строке вызова. Каждый аргумент — отдельная строка. Висячая запятая после последнего аргумента — ruff format ставит её автоматически, diff при добавлении аргумента будет однострочным.

Перенос перед бинарным оператором (Knuth-style)

is_eligible = (
    order.status == OrderStatus.CONFIRMED
    and order.amount >= MINIMUM_REFUND_AMOUNT
    and not order.is_refunded
)

Оператор в начале новой строки — сигнал «продолжение выражения». Оператор в конце предыдущей строки легко пропустить при скане сверху вниз.

total = (
    product.base_price
    + product.tax_amount
    - discount.value
)

Длинная цепочка вызовов

confirmed_orders = (
    session.query(Order)
    .filter(Order.status == OrderStatus.CONFIRMED)
    .filter(Order.customer_id == customer_id)
    .order_by(Order.created_at.desc())
    .all()
)

Каждый .method() — на отдельной строке с отступом 4 пробела. Цепочка читается сверху вниз, каждый шаг виден.

Горизонтальное выравнивание запрещено

PY-5.3: не выравнивать присваивания и словари горизонтально.

customer_id   = UUID("...")   # ✗ — выравнено пробелами
order_status  = OrderStatus.PENDING
total_amount  = Decimal("0.00")

customer_id = UUID("...")     # ✓
order_status = OrderStatus.PENDING
total_amount = Decimal("0.00")
config = {
    "host":     "localhost",   # ✗ — выравнено двоеточие
    "port":     5432,
    "database": "orders",
}

config = {
    "host": "localhost",       # ✓
    "port": 5432,
    "database": "orders",
}

Почему не выравнивать:

  1. Diff-noise при переименовании. Добавили payment_reference_id — нужно переформатировать все остальные строки, чтобы выравнивание сохранилось. Diff показывает N изменений вместо 1.
  2. ruff format откатывает. Форматтер не сохраняет горизонтальное выравнивание — при следующем прогоне «красивый» блок рассыплётся.
  3. Субъективность. Кто-то выравнивает по самому длинному имени текущего блока, кто-то — по следующему полю. Единое правило «без выравнивания» снимает вопрос навсегда.

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

PY-RUFF-1: конфиг ruff — только в pyproject.toml, не в разрозненных .flake8/.isort.cfg/setup.cfg.

[tool.ruff]
line-length = 88
target-version = "py312"

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

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

PY-RUFF-3: все три прогона в CI — блокируют merge при нарушении:

- run: ruff check src/ tests/
- run: ruff format --check src/ tests/
- run: mypy src/

Отключение правил ruff без обоснования — нарушение PY-RUFF-X1. Если правило реально мешает конкретному случаю:

result = long_function_name(arg_one, arg_two)  # noqa: E501  # justify: URL в строке, не сокращать

# noqa только с кодом правила и justify-комментарием (PY-7.X2).

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

АнтипаттернПравилоЧто взамен
Ручное форматирование вместо ruff formatPY-5.1ruff format src/
Разная длина строки в разных файлах проектаPY-5.2единый line-length в pyproject.toml
Горизонтальное выравнивание присваиванийPY-5.3без выравнивания
Горизонтальное выравнивание значений словаряPY-5.3без выравнивания
# noqa без кода правила и justifyPY-RUFF-3, PY-7.X2# noqa: E501 # justify: ...
Конфиг ruff в .flake8 или setup.cfgPY-RUFF-1pyproject.toml [tool.ruff]
Отключение правил ruff «потому что мешают»PY-RUFF-X1обсудить и зафиксировать override
Оператор +/and/or в конце строки при переносеPEP 8 A.4оператор в начале новой строки

Куда дальше

  • Именование — snake_case, PascalCase, константы, приватность.
  • Импорты — абсолютные импорты, группировка stdlib/third-party/local, запрет wildcard.
  • Тайп-хинты — X | None вместо Optional, Protocol для портов, Decimal для денег.
  • Enforcement через ruff и mypy — полная конфигурация CI, suppression-политика.
  • Раздел «Форматирование» в Python Style Guide — нормативные формулировки PY-5.*.