$ sudo teach IT
Модуль 5 · Правила

Популярные правила на практике

Разбираем самые важные правила с примерами кода и глубокими объяснениями.

🔧 Практика 🕓 ~30 минут
💡 О чём этот урок: Здесь мы подробно разбираем самые популярные правила Ruff, которые вы будете встречать в каждом проекте. Для каждого правила — объяснение, примеры плохого и хорошего кода, частые ошибки и советы.
F401

imported-but-unused (неиспользуемый импорт)

Правило F401 из категории Pyflakes — одно из самых частых. Оно находит импорты, которые нигде не используются в файле. Неиспользуемые импорты загрязняют неймспейс, замедляют загрузку модуля и могут маскировать ошибки.

Базовый пример

❌ Плохо
import os       # F401 — os не используется
import sys      # F401 — sys не используется

print("hello")
✅ Хорошо
print("hello")  # лишние импорты убраны

Особые случаи

Есть ситуации, когда импорт нужен, но напрямую не используется в коде файла:

Сценарий Решение Пример
Реэкспорт в __init__.py # noqa: F401 или per-file-ignores from .module import Class # noqa: F401
Side-effect импорт Комментарий или noqa import settings # noqa: F401
Импорт для type hints Используйте TYPE_CHECKING from typing import TYPE_CHECKING

Реэкспорт в __init__.py

# my_package/__init__.py
from my_package.core import MyClass  # реэкспорт для публичного API
from my_package.utils import helper

__all__ = ["MyClass", "helper"]

Лучше использовать __all__ вместе с noqa:

[tool.ruff.lint.per-file-ignores]
"**/__init__.py" = ["F401"]

TYPE_CHECKING — импорты для типов

Если вы импортируете класс только для аннотаций, используйте идиому с TYPE_CHECKING. Это предотвращает циклические импорты и ускоряет загрузку:

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from models import User  # не будет F401 — импорт только для type checker

def greet(user: User) -> str:
    return f"Hello, {user.name}"

Автоисправление F401

Ruff может автоматически удалять неиспользуемые импорты:

ruff check --fix --select F401
# Удалит все неиспользуемые импорты
⚠️ Предупреждение: Автоисправление F401 удаляет импорт целиком. Если у вас есть импорт с побочным эффектом (например, регистрация сигналов), используйте # noqa: F401 или unfixable = ["F401"].
F841

local-variable-is-assigned-to-but-never-used (неиспользуемая переменная)

Правило F841 находит локальные переменные, которым присваивается значение, но которые никогда не читаются. Это часто указывает на логические ошибки или забытый код.

Базовый пример

def foo():
    result = compute()   # F841 — result нигде не используется
    return "done"
❌ Плохо
def process_data(raw):
    cleaned = clean(raw)    # F841 — cleaned не используется
    transformed = transform(raw)  # F841
    return raw
✅ Хорошо
def process_data(raw):
    cleaned = clean(raw)
    transformed = transform(cleaned)
    return transformed

Особые случаи

Иногда переменная не нужна — например, вы хотите просто распаковать кортеж:

# Вместо:
first, second, third = get_values()  # third не используется

# Используй _:
first, second, _ = get_values()  # _ — соглашение о неиспользуемом значении

Пример с функцией, возвращающей кортеж:

# Плохо:
status, data = api.get_user(42)  # status не используется → F841

# Хорошо (имя _):
_, data = api.get_user(42)

# Или явное игнорирование:
status, data = api.get_user(42)
assert status == 200  # теперь status используется

Использование для отладки

Если вы присваиваете переменную для отладки (например, в pdb.set_trace()), Ruff всё равно будет ругаться:

def complex_function():
    result = expensive_call()  # F841 — даже если это для отладки
    # ... много кода
    # планировалось использовать result, но забыли
💡 Совет: Используйте _ для значений, которые вы намеренно игнорируете. Это не только подавляет F841, но и сообщает читателю кода: "это значение не важно".

Статистика: почему это важно

Неиспользуемые переменные — один из самых частых источников багов:

  • Забыли использовать результат функции
  • Перепутали переменные (написали x вместо y)
  • Оставили отладочный код в production
  • Не завершили рефакторинг
B006

mutable-argument-default (мутируемый аргумент по умолчанию)

Классическая Python-ловушка. Если использовать изменяемый объект (список, словарь, множество) как значение по умолчанию для аргумента функции, этот объект создаётся один раз при определении функции и разделяется между всеми вызовами.

Демонстрация проблемы

# Модуль 1: определение
def append_to(item, lst=[]):   # B006 — мутируемый дефолт
    lst.append(item)
    return lst

# Модуль 2: использование
result1 = append_to(1)
result2 = append_to(2)
print(result1)  # [1, 2] — а ожидалось [1]!
print(result2)  # [1, 2] — а ожидалось [2]!

Правильное решение

def append_to(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

result1 = append_to(1)  # [1]
result2 = append_to(2)  # [2] — правильно!

Другие мутируемые типы

Тип Плохо Хорошо
list lst=[] lst=None
dict dct={} dct=None
set items=set() items=None
dataclass field items: list = [] items: list = field(default_factory=list)

B008 — вызов функции в дефолте

Смежное правило B008 (function call in default argument) предупреждает о вызовах функций в значениях по умолчанию:

# B008: datetime.now() будет вызван один раз при определении функции!
def log(msg, time=datetime.now()):  # B008
    print(f"[{time}] {msg}")

log("start")  # время 10:00:00
log("end")    # время 10:00:00 — время не обновилось!
def log(msg, time=None):  # ✅
    if time is None:
        time = datetime.now()
    print(f"[{time}] {msg}")
⚠️ Исключение: В FastAPI и Pydantic вызов функций в дефолтах — нормальная практика (например, Field(default_factory=...) ). Отключите B008 для таких файлов через per-file-ignores.
UP006/UP007

Устаревшие type hints

Python постоянно развивается, и синтаксис аннотаций типов упрощается. Правила UP006 и UP007 предлагают заменить устаревшие конструкции из typing на современные встроенные типы.

UP006 — замена типов из typing на builtins (Python 3.9+):

Старый стиль (< 3.9) Новый стиль (3.9+)
List[str]list[str]
Dict[str, int]dict[str, int]
Tuple[int, str]tuple[int, str]
Set[str]set[str]
Frozenset[int]frozenset[int]
Type[Base]type[Base]

UP007 — замена Optional/Union на | (Python 3.10+):

Старый стиль (< 3.10) Новый стиль (3.10+)
Optional[str]str | None
Union[str, int]str | int
Optional[Union[str, int]]str | int | None
Union[str, List[int]]str | list[int]

Полный пример до/после

from typing import List, Dict, Optional, Union, Set, Tuple

def process(
    items: List[str],          # UP006 → list[str]
    mapping: Dict[str, int],   # UP006 → dict[str, int]
    prefix: Optional[str],     # UP007 → str | None
    timeout: Union[int, float], # UP007 → int | float
    tags: Set[str],            # UP006 → set[str]
    data: Tuple[int, ...],     # UP006 → tuple[int, ...]
) -> Optional[Dict[str, List[int]]]:  # оба правила
    ...
from typing import List, Dict, Optional, Union  # удалить лишние импорты

# Python 3.9+:
def process(
    items: list[str],
    mapping: dict[str, int],
    prefix: str | None,          # Python 3.10+
    timeout: int | float,        # Python 3.10+
    tags: set[str],
    data: tuple[int, ...],
) -> dict[str, list[int]] | None:  # Python 3.10+
    ...

Настройка target-version

Правила UP006 и UP007 учитывают target-version:

[tool.ruff]
target-version = "py310"  # включит и UP006, и UP007

# Если target-version = "py38":
# UP006 не будет предлагать builtins (только 3.9+)
# UP007 не будет предлагать | (только 3.10+)
target-version = "py38"  → не предлагает UP006, UP007
target-version = "py39"  → предлагает UP006 (builtins), не предлагает UP007 (|)
target-version = "py310" → предлагает UP006 и UP007
💡 Совет: Если вы поддерживаете Python 3.8, добавьте from __future__ import annotations в начале файлов. Это отложит вычисление аннотаций, и вы сможете использовать новый синтаксис даже с Python 3.8.
I001

Сортировка импортов

Правило I001 проверяет, что импорты отсортированы и сгруппированы по стандарту PEP 8 и isort. Ruff может автоматически сортировать импорты при сохранении — это одна из самых полезных возможностей.

Правильный порядок импортов

  1. Стандартная библиотека Python — os, sys, json, typing, pathlib...
  2. Третьесторонние библиотеки — django, flask, requests, numpy...
  3. Локальные модули — from myproject, from app, from .models...

Внутри каждой группы импорты сортируются по алфавиту.

❌ Плохо
import sys
import os
from pathlib import Path
import json
from django.db import models
import requests
from myproject.utils import helper
import typing
from flask import Flask
✅ Хорошо
import json          # stdlib
import os
import sys
import typing
from pathlib import Path

import requests      # third-party
from django.db import models
from flask import Flask

from myproject.utils import helper  # local

Настройка групп импортов

Вы можете настроить, какие библиотеки считаются третьесторонними, а какие — локальными:

[tool.ruff.lint.isort]
known-third-party = ["django", "flask", "fastapi", "sqlalchemy"]
known-first-party = ["myproject", "tests", "app"]
extra-standard-library = ["tomllib", "zoneinfo"]

# Разделение импортов на большее количество групп
section-order = [
    "future",       # from __future__ import annotations
    "standard",     # stdlib
    "third-party",  # библиотеки
    "first-party",  # локальные
    "local",        # .импорты
]

Автоисправление I001 в редакторе

Настройка VS Code для автосортировки импортов при сохранении:

{
  "[python]": {
    "editor.codeActionsOnSave": {
      "source.organizeImports.ruff": "explicit"
    }
  }
}
E501

line-too-long (слишком длинная строка)

Одно из самых спорных правил. E501 проверяет, что длина строки не превышает заданного лимита (по умолчанию 88 символов, как у Black).

Настройка лимита

[tool.ruff]
line-length = 100  # увеличить до 100

# или для Django:
line-length = 120  # Django проекты часто имеют длинные строки
❌ Плохо (line-length = 88)
class VeryLongClassName:
    def very_long_method_name(self, first_parameter, second_parameter, third_parameter):
        return self.some_attribute.calculate(first_parameter, second_parameter, third_parameter)
✅ Хорошо
class VeryLongClassName:
    def very_long_method_name(
        self, first_parameter, second_parameter, third_parameter
    ):
        return self.some_attribute.calculate(
            first_parameter, second_parameter, third_parameter
        )

Техники сокращения длинных строк

Метод Пример
Перенос аргументов Каждый аргумент на новой строке
Промежуточные переменные result = long.calc(x); print(result)
Родительские скобки Оборачивание в () для неявного переноса
Сокращение имён Только если это не ухудшает читаемость
# Перенос с помощью скобок (implicit line continuation):
result = (
    some_long_function_name()
    .method_chain()
    .another_method()
)

# Промежуточная переменная:
# Вместо:
if some_very_long_condition_that_takes_too_much_space() and another_condition():
    pass
# Лучше:
condition = some_very_long_condition_that_takes_too_much_space()
if condition and another_condition():
    pass
⚠️ Совет: E501 — самое часто отключаемое правило. Если в проекте много длинных строк (Django модели, SQL запросы), просто увеличьте line-length до 100-120 или отключите E501 в ignore. Помните: цель — читаемость кода, а не слепое следование лимиту.
➕

Другие популярные правила

F541 — f-string without placeholders

f-строки без подстановок излишни — используйте обычную строку:

x = f"hello"  # F541 → x = "hello"

F811 — redefinition of unused variable

def foo():
    x = 1
    x = 2  # F811 — x переопределён без использования первого значения
    return x

F821 — undefined name

print(undefined_variable)  # F821 — переменная не определена

N801 — class name must be CamelCase

class my_class:  # N801 → class MyClass:
    pass

S101 — assert used

assert user.is_admin  # S101 — assert удаляется с -O флагом

T201 — print statement

print(f"User {user.id} logged in")  # T201 → logger.info(...)

SIM108 — if-else to ternary

if condition:
    x = 1
else:
    x = 2
# SIM108 → x = 1 if condition else 2

RUF100 — unused noqa

import os  # noqa: F401  # RUF100 — F401 уже отключён в конфиге
💡 Совет: Чтобы узнать контекст любого правила, используйте ruff rule <CODE>. Например, ruff rule F541 покажет полное описание с примерами.
📈

Статистика: самые частые нарушения

По данным анализа open-source проектов на GitHub, вот самые частые правила, которые находит Ruff:

Место Правило Частота Описание
1F401Очень частоНеиспользуемый импорт
2F841ЧастоНеиспользуемая переменная
3E501Очень частоСлишком длинная строка
4I001ЧастоНесортированные импорты
5E302ЧастоОжидается 2 пустые строки
6UP006ЧастоУстаревшие type hints
7B006СреднеМутируемый аргумент
8N801СреднеИмя класса не CamelCase
9W291СреднеПробелы в конце строки
10F541Среднеf-string без подстановок
🛠

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

Рассмотрим типичный файл, который нарушает сразу несколько правил, и исправим его:

❌ До (с нарушениями)
import os, sys
from typing import List, Optional
import json

def process(items: List[str]) -> Optional[dict]:
    result = compute()
    for i in range(len(items)):
        print(f"Processing {items[i]}")
    return None
✅ После (исправлено)
import json
import os
import sys

def process(items: list[str]) -> dict | None:
    result = compute()
    for item in items:
        print(f"Processing {item}")
    return None

Что было исправлено:

Проблема Правило Исправление
Импорты на одной строкеE401Разделить на отдельные строки
Несортированные импортыI001Отсортировать по группам
List[str] (устаревший тип)UP006list[str]
Optional[dict]UP007dict | None
C-стиль индексацииPERF101 / C416for item in items
print() в productionT201logging.debug()
📝

Итоги

💡 Ключевые правила для каждого проекта:
  1. F401 — удаляйте неиспользуемые импорты, используйте __all__ и TYPE_CHECKING.
  2. F841 — неиспользуемые переменные — частый источник багов, используйте _.
  3. B006 — избегайте мутируемых дефолтов, используйте None + проверку.
  4. UP006/UP007 — обновляйте type hints до современного синтаксиса.
  5. I001 — держите импорты в порядке (Ruff сделает это автоматически).
  6. E501 — настройте комфортный лимит длины строки (88-120).
  7. Используйте ruff check --fix для автоматического исправления большинства проблем.
  8. Добавьте F401, F841, B006, UP, I в extend-select для всех проектов.

Урок 5.2: Популярные правила

5 вопросов