Learning Platform
Урок 03.03 · 20 мин
Начальный
ruffLintingFormattingpre-commitCode quality
Git hooks: автоматизация перед коммитом GitHub Actions: CI для Python-проектов

Зачем линтер для одного человека

В прошлом уроке мы заполнили pyproject.toml зависимостями. Но самый частый инструмент, который читает этот файл, ещё не подключён —

линтер
.

Junior часто думает: «у меня код работает, зачем мне линтер». Ответ из практики:

  • 80% реальных PR-комментариев от ревьюера на джунских PR — это стиль (имя переменной, длинная строка, лишний импорт). Линтер закрывает эти комментарии до того, как ревьюер открыл PR.
  • Половина «загадочных багов» в большом проекте — это
    unused variables
    и опечатки в именах, которые линтер ловит за секунду.
  • Единый стиль кода в команде из 5 человек экономит часы при чтении чужих PR — вы не отвлекаетесь на пробелы, концентрируетесь на логике.

С 2024 года стандарт для Python-проектов —

ruff
. Один бинарник, заменяющий пять-десять старых инструментов, в 10-100 раз быстрее.

История стека до ruff

В 2018-2023 годах в pyproject.toml любого приличного проекта были:

  • flake8 — линтер (правила PEP 8, неиспользуемые импорты, undefined variables).
  • black
    форматтер
    с философией «zero configuration», который форматировал код в один стиль без обсуждений.
  • isort — сортировка импортов в стандартном порядке.
  • pylint — более глубокий линтер с проверкой логики (часто отключали из-за false positives).
  • pyupgrade — автоматический апгрейд старого Python-синтаксиса на новый.

Каждый со своим конфигом, своими версиями, своими таймингами. На проекте в 50k строк запустить полный цикл занимало 30-60 секунд.

ruff объединил всё это в один бинарник на Rust. Запуск на тех же 50k строк — 200-500 миллисекунд. Это качественный сдвиг: линтер запускается так быстро, что его можно вешать на каждое сохранение файла в IDE — и вы видите ошибки в реальном времени.

Стек до ruff vs ruff в 2026

Один инструмент покрывает всё, что раньше делали четыре-пять отдельных программ.

2018-20235 инструментов
flake8линтинг PEP 8
blackформатирование
isortсортировка импортов
pylintглубокая проверка
pyupgradeапгрейд синтаксиса
20261 бинарник
ruff checkлинтинг (800+ правил)
ruff formatформатирование
правило Iсортировка импортов
правила B, RUFглубокая проверка
правило UPапгрейд синтаксиса

Установка и первый запуск

ruff — это CLI-утилита, поэтому ставим её через uv tool:

uv tool install ruff

Теперь команда ruff доступна из любой папки.

Базовый запуск:

ruff check .

Точка значит «рекурсивно от текущей папки». ruff пройдёт все .py-файлы и выдаст список нарушений:

src/etl.py:3:1: F401 [*] `os` imported but unused
src/etl.py:18:80: E501 Line too long (102 > 79)
src/etl.py:25:5: B007 Loop control variable `i` not used within loop body

Каждая строка — файл:строка:колонка: правило сообщение. Префикс правила говорит, из какой группы оно: F — pyflakes (логические ошибки), E — pycodestyle (PEP 8), B — bugbear (распространённые баги).

Автоматически исправить то, что можно исправить безопасно:

ruff check --fix .

ruff уберёт неиспользуемые импорты, переотсортирует импорты, апгрейднет старый синтаксис. То, что нельзя исправить автоматически (например, переименовать переменную), останется в выводе.

Форматирование — отдельная команда:

ruff format .

Это аналог black — переформатирует все .py-файлы по правилам форматирования. Конфликтов с ruff check нет, инструменты в одном бинарнике согласованы.

TIP

Правило большого пальца: сначала запускайте ruff format, потом ruff check --fix. Форматтер может разнести строку на несколько — и после этого линтер не сработает ложно на её длину.

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

Дефолты ruff разумные, но в каждом проекте обычно что-то добавляют. Конфиг живёт там, где должен — в pyproject.toml:

[tool.ruff]
line-length = 100
target-version = "py313"
extend-exclude = ["migrations", "vendored"]

[tool.ruff.lint]
select = [
    "E",    # pycodestyle errors
    "F",    # pyflakes
    "I",    # isort
    "B",    # flake8-bugbear
    "UP",   # pyupgrade
    "RUF",  # ruff-specific rules
]
ignore = ["E501"]  # line-too-long — оставляем форматтеру

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

Что здесь происходит:

  • line-length = 100 — мягкая граница строки. PEP 8 говорит 79, но в современных проектах 100-120 норма.
  • target-version = “py313” — целевая версия Python. От неё зависят правила UP: на 3.13 ruff подскажет заменить Optional[X] на X | None, на 3.8 — не подскажет.
  • select — список включённых групп правил. По умолчанию включены только E и F. Список выше — разумный «средний» набор.
  • ignore — отключение конкретных правил.
  • extend-exclude — папки, которые не сканировать (миграции БД, vendored-код).

Полный список правил — на странице

docs.astral.sh/ruff/rules/
. Их около 800. Не пытайтесь включить все — это путь к ненавидящей вас команде. Начинайте с минимума, добавляйте по мере накопления опыта.

Per-file-ignores

Иногда нужно отключить правило в одном конкретном файле — например, в __init__.py мы намеренно реэкспортируем символы, и правило F401 (unused import) ругается:

[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
"tests/*" = ["S101"]  # assert allowed in tests

Это глобально для проекта. Если хотите отключить правило в одной строке — комментарий # noqa:

import unused_module  # noqa: F401

Без указания правила # noqa отключит все правила в строке — так делать не рекомендуется, всегда указывайте конкретное.

ruff в IDE

В VSCode установите расширение «Ruff» от Astral. В PyCharm — встроенная интеграция с 2024.2 или плагин Ruff. После настройки:

  • При сохранении файла — автоформат и автоисправление.
  • В реальном времени подчёркиваются ошибки.
  • Hover на ошибку — описание правила и почему так.

Конфиг IDE читает из pyproject.toml — то есть для команды настройка единая, без личных .idea/ или settings.json.

pre-commit: ловить грязь до коммита

Линтер запускается локально, и есть один риск — забыть его запустить. Вы написали код, сразу git commit -m "wip" — и в репозитории оказалась грязь. Чтобы такого не было, есть инструмент

pre-commit
.

Git-хуки
существуют со времён git’а, но управлять ими руками — мучение: каждый разработчик должен сам скопировать скрипт в .git/hooks/. pre-commit решает это: один конфиг в репозитории, одна команда установки, дальше хук работает автоматически.

Установка и настройка

Поставим как CLI-утилиту:

uv tool install pre-commit

В корне проекта создайте .pre-commit-config.yaml:

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.7.4
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
        args: ['--maxkb=1000']

Что здесь:

  • Первый блок — официальные хуки ruff. ruff--fix) исправит, что может. ruff-format отформатирует.
  • Второй блок — служебные хуки: убрать пробелы в конце строк, гарантировать перевод строки в конце файла, проверить YAML на синтаксис, не дать закоммитить файл больше 1 MB.

Установка хука в свой git:

pre-commit install

Теперь при каждом git commit запускается список хуков. Если что-то падает — коммит отменяется, файлы остаются как были (или как поправил автофиксер), вы исправляете и коммитите снова.

Запустить хуки на всех файлах вручную:

pre-commit run --all-files

Как это выглядит в работе

Вы пишете код с ошибкой:

import os
import requests

def fetch():
    return requests.get('https://api.example.com')

(os импортирован, но не используется.)

Делаете коммит:

$ git add fetch.py
$ git commit -m "add fetch"
ruff (legacy alias)...........................................Failed
- hook id: ruff
- exit code: 1
- files were modified by this hook

Found 1 error (1 fixed, 0 remaining).

ruff format..................................................Passed
trim trailing whitespace.....................................Passed
end-of-file-fixer............................................Passed

ruff нашёл проблему, исправил автоматически (убрал os), но коммит отменился — изменённые файлы теперь в рабочей копии, и нужно их заново add. Просто повторяете:

$ git add fetch.py
$ git commit -m "add fetch"

— теперь все хуки проходят, коммит создаётся.

WARNING

Не используйте git commit --no-verify, чтобы пропустить хуки. Это типичная джунская привычка — «у меня и так работает». Через неделю в репозитории накопятся проблемы, которые хуки должны были ловить, и кто-то будет долго и тоскливо их разгребать. Если хук падает — разберитесь, почему, а не обходите.

pre-commit в CI

Pre-commit ловит грязь на машине разработчика. Но что, если кто-то не установил хуки локально? Чтобы такого не было, тот же pre-commit run --all-files запускают в

CI
:

# .github/workflows/lint.yml
name: lint
on: [push, pull_request]
jobs:
  pre-commit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v3
      - run: uv tool install pre-commit
      - run: pre-commit run --all-files

Так гарантируется, что ни один коммит в main не идёт в обход хуков, даже если автор отключил pre-commit install.

ruff vs ty: что нас ждёт дальше

Astral в 2025 году начала разработку

ty
— нового type-checker’а на Rust, который должен сделать с типизацией то же, что ruff сделал с линтингом: заменить mypy и pyright одним быстрым бинарником.

На 2026 год ty ещё бета, в production используют mypy или pyright. Но через год-полтора, скорее всего, стек упростится ещё: uv + ruff + ty будут покрывать 90% потребностей в инструментах.

Полный курс типов разберём в модуле «Типы и валидация». Пока запомните, что mypy/pyright — для type-checking, ruff — для всего остального.

Упражнение

  1. Создайте проект ruff-experiment, инициализируйте через uv init.

  2. Создайте файл dirty.py:

import os
import sys
import json
from pathlib import Path
def process_users(   users_list ):
    result=[]
    for u in users_list:
        if u['active']==True:
            result.append({'id':u['id'],'name':u['name']})
    return result
unused_var = 42
  1. Добавьте конфиг в pyproject.toml:
[tool.ruff]
line-length = 100
target-version = "py313"

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]
  1. Запустите ruff check dirty.py — посмотрите, сколько нарушений.

  2. Запустите ruff format dirty.py и ruff check --fix dirty.py.

Критерии приёмки:

  • После форматирования и автофикса в dirty.py не должно быть import os, import json, import sys (они не используются).
  • Импорты отсортированы по группам.
  • Код отформатирован: пробелы вокруг =, нет двойных пробелов в скобках.
  • Остаётся одно нарушение, которое автофикс не убирает — unused_var. Его нужно удалить руками.
  • Финальный ruff check dirty.py должен возвращать All checks passed!.

В следующем уроке — .env и pydantic-settings: как правильно хранить конфигурацию приложения, чтобы пароли не попали в git.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5