Зачем линтер для одного человека
В прошлом уроке мы заполнили pyproject.toml зависимостями. Но самый частый инструмент, который читает этот файл, ещё не подключён —
Junior часто думает: «у меня код работает, зачем мне линтер». Ответ из практики:
- 80% реальных PR-комментариев от ревьюера на джунских PR — это стиль (имя переменной, длинная строка, лишний импорт). Линтер закрывает эти комментарии до того, как ревьюер открыл PR.
- Половина «загадочных багов» в большом проекте — это и опечатки в именах, которые линтер ловит за секунду.unused variables
- Единый стиль кода в команде из 5 человек экономит часы при чтении чужих PR — вы не отвлекаетесь на пробелы, концентрируетесь на логике.
С 2024 года стандарт для Python-проектов —
История стека до 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 — это 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 нет, инструменты в одном бинарнике согласованы.
Правило большого пальца: сначала запускайте 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-код).
Полный список правил — на странице
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" — и в репозитории оказалась грязь. Чтобы такого не было, есть инструмент
.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"
— теперь все хуки проходят, коммит создаётся.
Не используйте git commit --no-verify, чтобы пропустить хуки. Это типичная джунская привычка — «у меня и так работает». Через неделю в репозитории накопятся проблемы, которые хуки должны были ловить, и кто-то будет долго и тоскливо их разгребать. Если хук падает — разберитесь, почему, а не обходите.
pre-commit в CI
Pre-commit ловит грязь на машине разработчика. Но что, если кто-то не установил хуки локально? Чтобы такого не было, тот же pre-commit run --all-files запускают в
# .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 году начала разработку
ruff сделал с линтингом: заменить mypy и pyright одним быстрым бинарником.
На 2026 год ty ещё бета, в production используют mypy или pyright. Но через год-полтора, скорее всего, стек упростится ещё: uv + ruff + ty будут покрывать 90% потребностей в инструментах.
Полный курс типов разберём в модуле «Типы и валидация». Пока запомните, что mypy/pyright — для type-checking, ruff — для всего остального.
Упражнение
-
Создайте проект
ruff-experiment, инициализируйте черезuv init. -
Создайте файл
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
- Добавьте конфиг в
pyproject.toml:
[tool.ruff]
line-length = 100
target-version = "py313"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]
-
Запустите
ruff check dirty.py— посмотрите, сколько нарушений. -
Запустите
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.