$ sudo teach IT
Модуль 3 · Форматирование

ruff format — замена Black

Автоматическое форматирование кода в стиле Black, только быстрее.

🎨 Форматирование 🕐 ~30 минут
🎯

Философия форматирования

Ruff format — это не просто очередной форматтер. Он разработан как drop-in замена Black — самого популярного Python-форматтера. Это означает, что ruff format стремится быть совместимым с Black на 99%+ и может заменить его в существующих проектах без изменения стиля кода.

Ключевые принципы ruff format:

  • Детерминированность — один и тот же код всегда форматируется одинаково
  • Минимальная конфигурация — почти все решения принимаются за вас
  • Совместимость с PEP 8 — форматтер следует официальному руководству по стилю
  • Скорость — ruff format работает в 10-100x быстрее Black
  • Magic trailing comma — уважает ваше решение о многострочности
Характеристика ruff format Black
Скорость (1000 файлов) ~0.1 сек ~2 сек
Язык Rust Python
Совместимость с Black ~99% 100%
Интеграция с линтером Встроенная (один инструмент) Требуется flake8 + isort
Magic trailing comma Да (по умолчанию) Да
Настройка quote-style Да (double/single) Нет (только double)
💡 Ruff format не стремится быть на 100% идентичным Black. Есть небольшие различия в edge cases (обработка комментариев, trailing commas в некоторых конструкциях). Но для подавляющего большинства проектов разница незаметна.
✨

Базовое использование

Команда ruff format форматирует Python-файлы на месте. В отличие от ruff check, она молча применяет изменения — вывод появляется только если файлы действительно изменились.

ruff format .               # форматировать всё в текущей директории
ruff format src/            # форматировать папку src
ruff format main.py         # форматировать один файл
ruff format src/ tests/     # несколько директорий
ruff format src/main.py src/utils.py  # несколько файлов

Ruff format обрабатывает файлы рекурсивно, включая все вложенные директории. По умолчанию он форматирует файлы с расширениями .py, .pyi, .pyx, .pyx.in.

🔄

Что форматирует ruff format

Ruff format изменяет только пробельные символы (пробелы, отступы, пустые строки) — он никогда не меняет семантику кода. Вот что он делает:

  • Отступы (замена табуляции на пробелы)
  • Пробелы вокруг операторов и после запятых
  • Перенос строк (line wrapping) при превышении line-length
  • Пустые строки между функциями и классами
  • Пробелы внутри скобок, списков, словарей
  • Тип кавычек (если настроен quote-style)
  • Magic trailing comma — многострочность коллекций

Пример: базовое форматирование

До
def foo(x,y,z):
    return x+y+z

result=foo(1,2,   3)
После ruff format
def foo(x, y, z):
    return x + y + z


result = foo(1, 2, 3)

Пример: перенос длинных строк

До
result = some_function_with_long_name(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)
После ruff format
result = some_function_with_long_name(
    arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8
)

Пример: многострочные коллекции

До
items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
После ruff format
items = [
    1,
    2,
    3,
    4,
    5,
    6,
    7,
    8,
    9,
    10,
    11,
    12,
    13,
    14,
    15,
]
📝

Больше примеров до/после

Пример: форматирование словаря

До
data = {"name": "Alice","age":30,"city": "Paris","job":"engineer"}
После ruff format
data = {
    "name": "Alice",
    "age": 30,
    "city": "Paris",
    "job": "engineer",
}

Пример: условные выражения

До
if x>0 and y<10 or z==5:
    print("hello")
elif a!=b:
    print("world")
После ruff format
if x > 0 and y < 10 or z == 5:
    print("hello")
elif a != b:
    print("world")

Пример: вложенные структуры

До
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
nested = {"key": {"inner": [1, 2, 3]}}
После ruff format
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
nested = {"key": {"inner": [1, 2, 3]}}

Вложенные структуры ruff format оставляет в одну строку, если они помещаются в лимит длины. Это decision от Black: краткость для простых вложенных структур.

Пример: long conditions

До
if is_authenticated and has_permission and not is_blocked and user.is_active and user.role == "admin":
    grant_access()
После ruff format
if (
    is_authenticated
    and has_permission
    and not is_blocked
    and user.is_active
    and user.role == "admin"
):
    grant_access()
🔍

--check — проверка без изменений

Флаг --check проверяет, нужно ли форматирование, не изменяя файлы. Если хотя бы один файл требует форматирования, ruff format --check завершается с кодом возврата 1. Это идеально для CI/CD.

ruff format --check .    # проверить нужно ли форматирование
# код возврата:
# 0 — все файлы отформатированы
# 1 — есть неотформатированные файлы

# пример для CI (GitHub Actions):
- name: Check formatting
  run: ruff format --check .
  # если код не отформатирован, CI упадёт

Exit codes ruff format:

Код Ситуация
0 Всё отформатировано (или --check: не требует форматирования)
1 Некоторые файлы не отформатированы (только с --check)
2 Ошибка выполнения (неверный синтаксис, проблемы с файлами)
👁️

--diff — предпросмотр изменений

Флаг --diff показывает unified diff всех изменений, которые будут применены, без фактического изменения файлов. Это стандартный способ просмотреть, что изменит форматтер, перед применением.

ruff format --diff .    # показать что изменится без применения
ruff format --diff main.py

# пример вывода:
# --- a/src/main.py
# +++ b/src/main.py
# @@ -1,4 +1,5 @@
# -def foo(x,y):
# -    return x+y
# +def foo(x, y):
# +    return x + y
# +
#  
# -result=foo(1,2,3)
# +result = foo(1, 2, 3)
💡 В CI обычно используют ruff format --check . — если код не отформатирован, сборка упадёт с кодом 1. Локально используйте ruff format --diff . для предпросмотра.
🪄

Magic trailing comma

Magic trailing comma (волшебная завершающая запятая) — это поведение, унаследованное от Black. Если в коллекции (списке, словаре, кортеже, наборе аргументов функции) присутствует запятая после последнего элемента, форматтер обязательно разобьёт коллекцию на несколько строк. Это ваш способ явно сказать форматтеру: "я хочу, чтобы это было многострочным".

# С trailing comma — всегда многострочное:
items = [
    "one",
    "two",
    "three",  # <-- trailing comma
]

# Без trailing comma — может схлопнуться в одну строку (если помещается):
items = ["one", "two", "three"]

# С trailing comma в аргументах функции:
def foo(
    x,
    y,
    z,  # <-- trailing comma
):
    pass
💡 Magic trailing comma — это ваш инструмент управления форматтером. Хотите сохранить многострочность? Добавьте trailing comma. Хотите разрешить схлопывание? Уберите её.
🌊

Fluent layout (цепочки вызовов)

Ruff format поддерживает стиль форматирования цепочек вызовов (fluent interface / method chaining). Если цепочка не помещается в одну строку, Ruff переформатирует её со смещением оператора:

# До:
result = dataframe.filter(col("age") > 18).groupBy("city").agg(avg("salary")).orderBy("city")

# После ruff format:
result = (
    dataframe.filter(col("age") > 18)
    .groupBy("city")
    .agg(avg("salary"))
    .orderBy("city")
)

# До:
query = db.select(User).where(User.age > 18).join(Profile).order_by(User.name).limit(10)

# После ruff format:
query = (
    db.select(User)
    .where(User.age > 18)
    .join(Profile)
    .order_by(User.name)
    .limit(10)
)

Этот стиль особенно популярен в проектах, использующих SQLAlchemy, Pandas, Django ORM и другие библиотеки с method chaining.

🔗

Совместная работа ruff check и ruff format

Важно понимать, что ruff check и ruff format — это две независимые команды. ruff check не вызывает ruff format, и наоборот. Рекомендуется запускать их последовательно:

# полный цикл проверки:
ruff check .           # найти проблемы
ruff check --fix .     # исправить что можно
ruff format .          # отформатировать код

# для CI:
ruff check --select E,F,I,N,UP,B .
ruff format --check .

# pre-commit хуки:
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format
⚖️

Отличия ruff format от Black

Хотя ruff format стремится к совместимости с Black, есть несколько отличий:

Аспект ruff format Black
Форматирование docstring Может форматировать код внутри docstring Не форматирует
Тип кавычек Настраиваемый (double/single) Только double
Trailing comma в функциях Следует Black, небольшие отличия Эталонное поведение
Обработка комментариев Отличия в edge cases Эталонное поведение
Zebra-striping (чередование строк) Не поддерживает Поддерживает (экспериментально)
Скорость 10-100x быстрее Медленнее
📖 Подробное сравнение: docs.astral.sh/ruff/formatter/#black-compatibility
🛠️

Практические сценарии

Сценарий 1: Миграция с Black

# Удалить Black из зависимостей
pip uninstall black
# или
poetry remove black
# Установить Ruff (если ещё не установлен)
pip install ruff
# Отформатировать проект
ruff format .
# Проверить, что форматирование совпадает
ruff format --check .

Сценарий 2: CI проверка форматирования

# GitHub Actions
- name: Check formatting
  run: ruff format --check .

# Makefile
format-check:
    ruff format --check .

format:
    ruff format .

Сценарий 3: Pre-commit интеграция

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.0
    hooks:
      - id: ruff
        args: [--fix, --select, I]
      - id: ruff-format

Сценарий 4: Форматирование только изменённых файлов

# отформатировать только файлы, изменённые в текущем коммите
ruff format $(git diff --name-only --diff-filter=AM | grep '\.py$')

# или через pre-commit — само применяется к staged файлам
💡 Используйте pre-commit с ruff-format, чтобы форматирование применялось автоматически при каждом коммите.
📝

Шпаргалка

Команда Назначение
ruff format . Отформатировать всё
ruff format --check . Проверить без изменений (для CI)
ruff format --diff . Предпросмотр изменений
ruff format main.py Форматировать один файл
📖 Документация: docs.astral.sh/ruff/formatter/
📄

Форматирование .pyi, .ipynb и других файлов

Ruff format поддерживает несколько типов файлов помимо обычных .py:

  • .py — обычные Python-файлы
  • .pyi — stub-файлы (type hints). Форматируются как py, но с некоторыми отличиями (например, не добавляются пустые строки между функциями в некоторых случаях)
  • .pyx — Cython-файлы. Ruff обрабатывает только Python-секции, директивы Cython не форматируются
  • .ipynb — Jupyter Notebooks. Ruff форматирует код внутри ячеек (требуется --preview или настройка)
# форматировать все типы файлов
ruff format .

# только stub-файлы
ruff format "*.pyi"

# Jupyter notebooks (нужно указать явно или настроить include)
ruff format notebook.ipynb
💡 Для Jupyter notebooks нужно убедиться, что расширение .ipynb включено в include в конфигурации. По умолчанию Ruff форматирует только .py, .pyi, .pyx.
💬

Обработка комментариев

Ruff format старается сохранять комментарии и их позицию относительно кода. Однако есть некоторые правила:

  • Комментарии на той же строке, что и код, остаются на месте
  • Комментарии между строками кода сохраняются
  • Если строка с комментарием переносится, комментарий остаётся с перенесённой частью (если возможно)
  • Docstring-комментарии ("""...""") могут быть переформатированы, если включена опция docstring-code-format
# Комментарии сохраняются:
def foo():  # это комментарий
    x = 1  # и этот
    # А этот останется между строками
    y = 2
    return x + y
⚡

Производительность ruff format

Одно из главных преимуществ Ruff перед Black — скорость. Ruff написан на Rust и использует параллельную обработку файлов.

Проект Файлов ruff format Black
CPython ~3000 ~0.4 сек ~8 сек
Django ~2000 ~0.3 сек ~6 сек
Small project ~50 ~0.02 сек ~0.5 сек
🔌

Форматирование через LSP

Ruff поставляется со встроенным LSP (Language Server Protocol) сервером. Это позволяет редакторам кода форматировать файлы через стандартный протокол LSP.

# Запуск LSP сервера Ruff
ruff server

# В VS Code — установите расширение Ruff
# В Neovim — используйте ruff_lsp
lvim config:
require("lspconfig").ruff.setup({
  cmd = { "ruff", "server" },
  on_attach = function(client, bufnr)
    -- Включить форматирование через LSP
    vim.api.nvim_buf_set_option(bufnr, "formatexpr", "v:lsp.formatexpr")
    vim.cmd("set formatprg=ruff\\ format\\ -")
  end,
})

Использование LSP обеспечивает форматирование в реальном времени, с подсветкой синтаксиса и мгновенным отображением результата.

🐛

Отладка форматирования

Если ruff format ведёт себя не так, как ожидалось, вот несколько способов отладки:

  • Используйте --diff чтобы увидеть, какие изменения будут применены
  • Проверьте ruff check --show-settings чтобы убедиться, что настройки правильные
  • Проверьте версию Ruff: ruff --version
  • Если поведение отличается от Black, проверьте документацию по совместимости
# отладка
ruff format --diff main.py | head -50
ruff check --show-settings
ruff --version
💡

Форматирование лямбд и генераторов

Ruff format обрабатывает лямбда-выражения и генераторные выражения особым образом. Лямбды по возможности остаются в одной строке, но если они слишком длинные — переносятся:

До
add = lambda x,y: x+y
process = lambda x, y, z: some_function(x, y, z) + another_function(x, y, z)
squares = [x*x for x in range(10) if x % 2 == 0]
После ruff format
add = lambda x, y: x + y
process = lambda x, y, z: some_function(x, y, z) + another_function(
    x, y, z
)
squares = [x * x for x in range(10) if x % 2 == 0]
📋

Форматирование объявлений функций

Объявления функций с длинными параметрами форматируются особым образом. Каждый параметр может быть на отдельной строке, если они не помещаются:

До
def process_data(input_file, output_file, format_type, encoding, overwrite=False, backup=True):
    pass
После ruff format
def process_data(
    input_file,
    output_file,
    format_type,
    encoding,
    overwrite=False,
    backup=True,
):
    pass

Если же все параметры помещаются на одной строке, они остаются на одной строке:

def add(x, y):  # помещается — одна строка
    return x + y
🏷️

Форматирование декораторов

Декораторы в Python могут быть многострочными, и ruff format аккуратно их обрабатывает:

# До:
@app.route("/api/v1/users//posts/", methods=["GET", "POST", "DELETE"])
def handle_post(user_id, post_id):
    pass

# После ruff format:
@app.route(
    "/api/v1/users//posts/",
    methods=["GET", "POST", "DELETE"],
)
def handle_post(user_id, post_id):
    pass
⭐

Best practices по форматированию

  1. Не боритесь с форматтером — приняв решение использовать ruff format, доверьтесь ему. Не тратьте время на ручное форматирование.
  2. Используйте trailing comma для контроля многострочности. Добавили запятую — форматтер оставит многострочным.
  3. Запускайте ruff format перед ruff check — форматирование может устранить некоторые ошибки E и W.
  4. В CI используйте --check — это гарантирует, что весь код в репозитории отформатирован одинаково.
  5. Настраивайте pre-commit для автоматического форматирования перед каждым коммитом.
❓

Часто задаваемые вопросы

Q: Чем отличается ruff format от ruff check --fix?

A: ruff format занимается только пробельным форматированием (отступы, переносы, пробелы). ruff check --fix исправляет семантические нарушения (удаление импортов, сортировка, замена конструкций). Это разные инструменты с разными целями.

Q: Может ли ruff format изменить мой код?

A: Нет. Ruff format меняет только пробельные символы: отступы, пробелы, пустые строки, переносы. Он никогда не меняет семантику кода. Это гарантируется дизайном форматтера.

Q: Нужно ли запускать ruff format перед ruff check?

A: Да, рекомендуется. Ruff check может находить ошибки, которые исчезнут после форматирования (например, пробелы). Порядок: ruff format . && ruff check .

Q: Как отформатировать только изменённые файлы?

A: Используйте ruff format $(git diff --name-only --diff-filter=AM | grep '\.py$') или настройте pre-commit.

Q: Поддерживает ли ruff format автозамену одинарных кавычек на двойные?

A: Да, через настройку quote-style. Если указать "double", Ruff будет заменять одинарные кавычки на двойные (и наоборот).

Урок 3.1: Форматирование кода — ruff format

5 вопросов