PEP 8, линтеры и чистый код

⏱ 30 мин · 🧩 заданий: 5

Дзен Python

import codecs, contextlib, io

with contextlib.redirect_stdout(io.StringIO()):
    import this               # «пасхалка»: печатает дзен, но только при первом импорте
print(codecs.decode(this.s, "rot13"))   # поэтому берём текст из модуля (он хранится в ROT13)

Главное из этого: «Читаемость важна», «Явное лучше неявного», «Простое лучше сложного». Код читают в десятки раз чаще, чем пишут.

PEP 8 — стиль кода

Имена

Что Стиль Пример
переменные, функции, методы, модули snake_case total_price, get_user(), order_service.py
классы, исключения PascalCase HttpClient, PaymentError
константы UPPER_CASE MAX_RETRIES = 3
внутреннее _leading_underscore _cache
конфликт с ключевым словом trailing_ class_, type_

Не используйте l, O, I как имена — их легко спутать с 1 и 0.

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

# ✓ 4 пробела отступа, пробелы вокруг операторов, после запятой
def calculate_total(items, tax_rate=0.2):
    subtotal = sum(item.price * item.qty for item in items)
    return subtotal * (1 + tax_rate)


# ✓ две пустые строки между функциями и классами верхнего уровня
class Order:
    def __init__(self, items):
        self.items = items

    def total(self):                         # одна пустая строка между методами
        return calculate_total(self.items)


# ✓ длинные выражения — переносы внутри скобок
result = some_function(
    first_argument,
    second_argument,
    keyword=value,
)

# ✗ так не надо
def f(a,b=1):return a+b
x=f( 1,b = 2 )
  • Длина строки — 79 символов по PEP 8; в командах обычно договариваются о 88–120 (black — 88).
  • is None / is not None, а не == None.
  • if items: вместо if len(items) > 0:; if flag: вместо if flag == True:.
  • isinstance(x, int) вместо type(x) == int.

Импорты

# 1. стандартная библиотека
import json
from pathlib import Path

# 2. сторонние пакеты
import httpx
from fastapi import FastAPI

# 3. ваш проект
from myapp.config import settings
from myapp.services import orders

Каждый импорт на своей строке, в начале файла, без import *.

Инструменты

Стиль никто не проверяет глазами — это делают инструменты, локально и на CI.

Инструмент Задача
ruff линтер + форматтер, очень быстрый (Rust); заменяет flake8, isort, pyupgrade, pydocstyle, black
black бескомпромиссный форматтер
mypy / pyright проверка типов
pre-commit запуск проверок перед каждым коммитом
EditorConfig единые отступы и кодировка во всех редакторах
# pyproject.toml
[tool.ruff]
line-length = 100
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "W", "I", "B", "UP", "SIM", "N"]   # pycodestyle, pyflakes, isort, bugbear, pyupgrade...

[tool.ruff.format]
quote-style = "double"
ruff check .            # найти проблемы
ruff check --fix .      # исправить автоматически
ruff format .           # отформатировать
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.9
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

Docstrings

def transfer(source: str, target: str, amount: int) -> str:
    """Перевести деньги между счетами.

    Args:
        source: Номер счёта списания.
        target: Номер счёта зачисления.
        amount: Сумма в копейках, больше нуля.

    Returns:
        Идентификатор транзакции.

    Raises:
        ValueError: Если сумма не положительная.
    """
    if amount <= 0:
        raise ValueError("сумма должна быть положительной")
    return f"tx-{source}-{target}-{amount}"

print(transfer.__doc__.splitlines()[0])
help(transfer)

Первая строка — краткое описание в повелительном наклонении. Стили: Google (как выше), NumPy, reStructuredText.

Чистый код

Понятные имена

# ✗
def proc(d, f):
    r = []
    for x in d:
        if x[2] > f:
            r.append(x)
    return r

# ✓
def filter_expensive(products, min_price):
    return [product for product in products if product.price > min_price]

Ранний возврат вместо вложенности

# ✗ «стрелка»
def can_publish(user, post):
    if user is not None:
        if user.is_active:
            if post.author_id == user.id or user.is_admin:
                return True
    return False

# ✓ сторожевые условия
def can_publish(user, post):
    if user is None or not user.is_active:
        return False
    return post.author_id == user.id or user.is_admin

Маленькие функции, одна задача

Функция делает одно дело, и её название говорит, какое. Если в названии появляется «и» (validate_and_save_and_notify) — пора разделять.

Без магических чисел

# ✗
if response.status == 429:
    time.sleep(60)

# ✓
from http import HTTPStatus
RATE_LIMIT_PAUSE_SECONDS = 60

if response.status == HTTPStatus.TOO_MANY_REQUESTS:
    time.sleep(RATE_LIMIT_PAUSE_SECONDS)

Комментарии объясняют «зачем»

Код показывает, что происходит. Комментарий нужен, чтобы объяснить почему так: неочевидное бизнес-правило, обход бага библиотеки, ссылку на задачу. Устаревший комментарий хуже отсутствующего.

Код-ревью

На ревью смотрят:

  1. Корректность — делает ли код то, что нужно, крайние случаи, ошибки.
  2. Тесты — есть ли, проверяют ли поведение.
  3. Читаемость — имена, структура, сложность.
  4. Безопасность и производительность — SQL-инъекции, N+1 запросов, секреты в коде.
  5. Стиль — должен проверяться автоматически, а не в комментариях ревьюера.

Комментируйте код, а не человека: «здесь может быть None, давай добавим проверку» вместо «ты опять забыл».

Вопрос на собеседовании

«Как вы поддерживаете качество кода в команде?» — Автоматический форматтер и линтер (ruff), проверка типов (mypy), pre-commit хуки и те же проверки на CI, обязательное код-ревью, тесты с контролем покрытия, договорённости в CONTRIBUTING.

Итог

  • PEP 8: snake_case для функций и переменных, PascalCase для классов, UPPER_CASE для констант, 4 пробела.
  • Импорты группами: стандартные → сторонние → свои.
  • ruff + mypy + pre-commit + CI — стиль проверяют инструменты.
  • Docstrings для публичного API; комментарии объясняют «зачем».
  • Понятные имена, маленькие функции, ранний возврат, никаких магических чисел.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Отрефакторьте функцию: сейчас она работает, но нарушает PEP 8 и читается плохо. Сохраните поведение, но: - функция должна называться calculate_discount (snake_case), параметры — price и is_vip; - уберите вложенные if через ранний возврат; - вынесите «магические числа» в константы VIP_DISCOUNT = 0.2 и BIG_ORDER_DISCOUNT = 0.1 и порог BIG_ORDER_THRESHOLD = 10000. Логика: VIP — скидка 20%, иначе при цене ≥ 10000 — 10%, иначе без скидки. Возвращает итоговую цену.

Какие тесты проверяют код (3)
assert calculate_discount(1000, True) == 800 and calculate_discount(20000, False) == 18000 and calculate_discount(500, False) == 500
assert (VIP_DISCOUNT, BIG_ORDER_DISCOUNT, BIG_ORDER_THRESHOLD) == (0.2, 0.1, 10000)
import inspect; assert list(inspect.signature(calculate_discount).parameters) == ["price", "is_vip"]
Задание 2 Заполнить таблицу

Как по PEP 8 называть?

snake_casePascalCaseUPPER_CASE
функцию get user by id
класс http client
константу max retries
переменную total price
модуль (файл) order service
исключение payment failed error
Отметьте ячейку — «+», пустая — «−».
Задание 3 Один вариант ответа

В каком порядке PEP 8 рекомендует группировать импорты?

Задание 4 Один вариант ответа

Какой комментарий полезен?

Задание 5 Один вариант ответа

Что делает ruff?

Войдите, чтобы отмечать пройденные уроки.