Опирается на правила:
PY-4.1…PY-4.6,PY-4.X1…PY-4.X3из Python Style Guide → раздел 4. Выражения и код.
Важно знать
- Guard clause (
PY-4.1) — раннийraise/returnвместо вложенныхif/else; счастливый путь идёт прямо.- Comprehension (
PY-4.2) — вместо ручногоappend-цикла; но вложенные и многоусловные — обычнымforради читаемости.- EAFP vs LBYL (
PY-4.3) —try/exceptдля ожидаемо-редких сбоев;if-проверка — когда она дешевле исключения.- f-string (
PY-4.4) — единственный способ форматирования строк;%/.format()и конкатенация запрещены.pathlib.Path(PY-4.5) — вместоos.path-конкатенации; файловые ресурсы — черезwith.- Булево выражение (
PY-4.6) — не более трёхand/or; сложнее — выделить именованный предикат.- Мутабельный default (
PY-4.X1) —def f(x=[])делит список между вызовами; всегдаNone+ инициализация в теле.- Голый
except(PY-4.X2) — без re-raise/обработки проглатывает ошибки молча.
Читаемость кода — ключевой критерий. Все правила раздела 4 направлены на одно: счастливый путь читается сверху вниз, ошибки и крайние случаи обрабатываются локально, а конструкции языка используются по назначению.
Guard clause — ранний return/raise
PY-4.1: ранний выход вместо роста вложенности.
Код с вложенными if/else требует удерживать стек условий в уме:
def charge(order: Order) -> None:
if not order.is_paid:
if order.amount > 0:
provider.charge(order)
Guard clause переворачивает логику — сначала проверяем нарушение и выходим, затем идёт основной путь:
def charge(order: Order) -> None:
if order.is_paid:
return
provider.charge(order)
Для ошибочных состояний guard clause использует raise:
def submit_order(order: Order, customer: Customer) -> None:
if order.is_empty():
raise OrderValidationError("order has no items")
if not customer.is_active:
raise CustomerBlockedError(customer.id)
payment_gateway.charge(customer, order.total)
order_repository.save(order)
Логика оплаты и сохранения читается без ментального отступа — guard clause убрал оба условия-нарушения вверх.
Три уровня вложенности внутри функции — сигнал, что функция делает слишком много. Guard clause помогает, но если вложенность возвращается — функцию стоит разбить.
Comprehension вместо append-цикла
PY-4.2: список или dict строится через comprehension, не через пустую коллекцию + append.
active_order_ids = [order.id for order in orders if order.is_active]
amounts_by_id = {order.id: order.amount for order in orders}
Вложенные comprehension быстро теряют читаемость. Если в comprehension больше одного if или двух for — обычный цикл предпочтительнее:
cancelled_items = []
for order in orders:
for item in order.items:
if item.status == ItemStatus.CANCELLED:
if item.price > Decimal("0"):
cancelled_items.append(item)
Граница — читаемость, а не формальное число уровней. Вложенный comprehension в четыре строки стоит превратить в цикл раньше.
Generator expression (без квадратных скобок) уместен там, где коллекция в памяти не нужна:
total = sum(item.price for item in order.items if item.is_billable)
EAFP и LBYL — когда что выбирать
PY-4.3: два стиля работы с потенциально-отсутствующими данными.
EAFP (Easier to Ask Forgiveness than Permission) — сначала пробуем, исключение ловим если что-то пошло не так. Подходит, когда успех — норма, а исключение — редкость:
def get_order_total(order_id: UUID) -> Decimal:
try:
order = order_repository.find_by_id(order_id)
return order.total
except OrderNotFoundError:
return Decimal("0")
LBYL (Look Before You Leap) — сначала проверяем, потом делаем. Подходит, когда проверка дешевле исключения или когда семантически важно различать «не существует» и «ошибка»:
def reserve_product(product_id: UUID, quantity: int) -> None:
product = product_repository.find_by_id(product_id)
if product is None:
raise ProductNotFoundError(product_id)
if product.stock < quantity:
raise InsufficientStockError(product_id, quantity, product.stock)
product_repository.reserve(product_id, quantity)
Выбор между EAFP и LBYL определяется доменом: если «не найдено» — часть нормального потока, LBYL с явной проверкой читается лучше. Если исключение действительно исключительно — EAFP.
Голый except: или except Exception: без обработки (pass) — нарушение PY-4.X2. Исключение должно либо перевыброситься с контекстом, либо быть явно задокументировано как ожидаемое.
f-string для форматирования строк
PY-4.4: f-string — единственный способ форматирования строк в Python 3.12+.
message = f"order {order.id} declined by {provider.name}: {reason}"
Старые стили запрещены:
message = "order %s declined by %s: %s" % (order.id, provider.name, reason)
message = "order {} declined by {}: {}".format(order.id, provider.name, reason)
message = "order " + str(order.id) + " declined by " + provider.name + ": " + reason
f-string поддерживает выражения, форматирование чисел и вызов методов:
summary = f"order {order.id}: {order.total:.2f} RUB, {len(order.items)} items"
status_line = f"customer {customer.full_name.upper()!r} status={customer.status}"
Для длинных строк f-string можно перенести без конкатенации:
log_message = (
f"payment failed for order {order.id} "
f"customer={customer.id} "
f"amount={order.total:.2f}"
)
pathlib.Path и контекст-менеджеры
PY-4.5: файловая система — через pathlib.Path, ресурсы — через with.
os.path-конкатенация непортируема, многословна и теряет тип пути:
import os
config_path = os.path.join(base_dir, "config", "settings.yaml")
with open(config_path) as f:
data = f.read()
pathlib.Path — объектная модель, оператор / безопасен на всех платформах:
from pathlib import Path
config_path = Path(base_dir) / "config" / "settings.yaml"
with config_path.open() as f:
data = f.read()
Любой ресурс — файл, сетевое соединение, транзакция — открывается через with. Это гарантирует освобождение ресурса даже при исключении:
async def export_orders(customer_id: UUID, output: Path) -> None:
orders = await order_repository.find_by_customer(customer_id)
with output.open("w", encoding="utf-8") as f:
for order in orders:
f.write(f"{order.id},{order.total}\n")
Сложность булева выражения
PY-4.6: не более трёх and/or в одном выражении.
Когда условие состоит из четырёх и более частей, его смысл размывается. Выделение именованного предиката восстанавливает читаемость:
def _is_eligible_for_discount(order: Order, customer: Customer) -> bool:
return (
customer.is_premium
and order.total >= Decimal("5000")
and not order.has_promotional_items
)
def apply_discount(order: Order, customer: Customer) -> Order:
if not _is_eligible_for_discount(order, customer):
return order
return order.with_discount(PREMIUM_DISCOUNT_RATE)
Имя _is_eligible_for_discount само является документацией — комментарий не нужен.
Оператор not с and/or повышает когнитивную нагрузку. Де Морган (not (A and B) = not A or not B) помогает упростить, но лучший выход — именованный предикат с позитивной формулировкой.
Мутабельный default — главная ловушка
PY-4.X1: мутабельный аргумент по умолчанию создаётся один раз при определении функции и разделяется между всеми вызовами.
def collect(items: list[str] = []) -> None:
items.append("x")
print(items)
collect()
collect()
Оба вызова печатают ["x"] и ["x", "x"] — список один и тот же. Правильно:
def collect(items: list[str] | None = None) -> None:
items = items if items is not None else []
items.append("x")
print(items)
То же для dict, set и любого другого мутабельного объекта. Иммутабельные значения (None, 0, "", False, кортежи) безопасны как default.
Изменение коллекции при итерации
PY-4.X3: добавление или удаление элементов из коллекции во время итерации по ней — неопределённое поведение.
for order in orders:
if order.is_expired:
orders.remove(order)
Итерация по копии или формирование нового списка:
active_orders = [order for order in orders if not order.is_expired]
Для словарей:
for key in list(product_cache.keys()):
if product_cache[key].is_stale():
del product_cache[key]
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
if not x: if y: do_thing() — вложенные условия | PY-4.1 | Guard clause: ранний return/raise |
result = []; for x in xs: result.append(f(x)) | PY-4.2 | result = [f(x) for x in xs] |
except Exception: pass без обработки | PY-4.X2 | Конкретное исключение + re-raise или явная обработка |
"order %s" % order_id или "order {}".format(order_id) | PY-4.4 | f"order {order_id}" |
os.path.join(base, "config", "file.yaml") | PY-4.5 | Path(base) / "config" / "file.yaml" |
def f(tags: list[str] = []) — мутабельный default | PY-4.X1 | def f(tags: list[str] \| None = None) + инициализация в теле |
a and b and c and d and e — 4+ операторов | PY-4.6 | Именованный предикат |
for x in col: col.remove(x) — мутация при итерации | PY-4.X3 | Итерация по копии или новый список через comprehension |
Куда дальше
- Именование —
snake_case,PascalCase, константы, приватность через_. - Тайп-хинты —
X | NoneвместоOptional,Protocol,Decimalдля денег. - ruff + mypy — конфиг в
pyproject.toml,mypy --strict,# noqaс обоснованием. - /standards/backend/error-handling/python/exception-hierarchy/ — иерархия исключений и где что ловить.