PEP 8, линтеры и чистый код
Дзен 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)
Комментарии объясняют «зачем»
Код показывает, что происходит. Комментарий нужен, чтобы объяснить почему так: неочевидное бизнес-правило, обход бага библиотеки, ссылку на задачу. Устаревший комментарий хуже отсутствующего.
Код-ревью
На ревью смотрят:
- Корректность — делает ли код то, что нужно, крайние случаи, ошибки.
- Тесты — есть ли, проверяют ли поведение.
- Читаемость — имена, структура, сложность.
- Безопасность и производительность — SQL-инъекции, N+1 запросов, секреты в коде.
- Стиль — должен проверяться автоматически, а не в комментариях ревьюера.
Комментируйте код, а не человека: «здесь может быть None, давай добавим проверку» вместо «ты опять забыл».
Вопрос на собеседовании
«Как вы поддерживаете качество кода в команде?» — Автоматический форматтер и линтер (ruff), проверка типов (mypy), pre-commit хуки и те же проверки на CI, обязательное код-ревью, тесты с контролем покрытия, договорённости в CONTRIBUTING.
Итог
- PEP 8:
snake_caseдля функций и переменных,PascalCaseдля классов,UPPER_CASEдля констант, 4 пробела. - Импорты группами: стандартные → сторонние → свои.
- ruff + mypy + pre-commit + CI — стиль проверяют инструменты.
- Docstrings для публичного API; комментарии объясняют «зачем».
- Понятные имена, маленькие функции, ранний возврат, никаких магических чисел.
Практика решено 0 из 5
Отрефакторьте функцию: сейчас она работает, но нарушает 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"]
VIP_DISCOUNT = 0.2
BIG_ORDER_DISCOUNT = 0.1
BIG_ORDER_THRESHOLD = 10000
def calculate_discount(price, is_vip):
if is_vip:
return price * (1 - VIP_DISCOUNT)
if price >= BIG_ORDER_THRESHOLD:
return price * (1 - BIG_ORDER_DISCOUNT)
return price
Как по PEP 8 называть?
| snake_case | PascalCase | UPPER_CASE | |
|---|---|---|---|
функцию get user by id |
|||
класс http client |
|||
константу max retries |
|||
переменную total price |
|||
модуль (файл) order service |
|||
исключение payment failed error |
В каком порядке PEP 8 рекомендует группировать импорты?
Какой комментарий полезен?
Что делает ruff?
Войдите, чтобы отмечать пройденные уроки.