Популярные правила на практике
Разбираем самые важные правила с примерами кода и глубокими объяснениями.
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
# Удалит все неиспользуемые импорты
# 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}")
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
from __future__ import annotations в начале файлов. Это отложит вычисление аннотаций, и вы сможете использовать новый синтаксис даже с Python 3.8.
I001
Сортировка импортов
Правило I001 проверяет, что импорты отсортированы и сгруппированы по стандарту PEP 8 и isort. Ruff может автоматически сортировать импорты при сохранении — это одна из самых полезных возможностей.
Правильный порядок импортов
- Стандартная библиотека Python — os, sys, json, typing, pathlib...
- Третьесторонние библиотеки — django, flask, requests, numpy...
- Локальные модули — 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 проекты часто имеют длинные строки
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
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:
| Место | Правило | Частота | Описание |
|---|---|---|---|
| 1 | F401 | Очень часто | Неиспользуемый импорт |
| 2 | F841 | Часто | Неиспользуемая переменная |
| 3 | E501 | Очень часто | Слишком длинная строка |
| 4 | I001 | Часто | Несортированные импорты |
| 5 | E302 | Часто | Ожидается 2 пустые строки |
| 6 | UP006 | Часто | Устаревшие type hints |
| 7 | B006 | Средне | Мутируемый аргумент |
| 8 | N801 | Средне | Имя класса не CamelCase |
| 9 | W291 | Средне | Пробелы в конце строки |
| 10 | F541 | Средне | 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] (устаревший тип) | UP006 | list[str] |
| Optional[dict] | UP007 | dict | None |
| C-стиль индексации | PERF101 / C416 | for item in items |
| print() в production | T201 | logging.debug() |
Итоги
- F401 — удаляйте неиспользуемые импорты, используйте __all__ и TYPE_CHECKING.
- F841 — неиспользуемые переменные — частый источник багов, используйте _.
- B006 — избегайте мутируемых дефолтов, используйте None + проверку.
- UP006/UP007 — обновляйте type hints до современного синтаксиса.
- I001 — держите импорты в порядке (Ruff сделает это автоматически).
- E501 — настройте комфортный лимит длины строки (88-120).
- Используйте
ruff check --fixдля автоматического исправления большинства проблем. - Добавьте F401, F841, B006, UP, I в extend-select для всех проектов.
Урок 5.2: Популярные правила
5 вопросов