Опирается на правила:
PY-8.1…PY-8.5из Python Style Guide → раздел 8. Современные фичи.
Важно знать
match/case— для разбора структур и sealed-подобных иерархий; заменяетisinstance-цепочки.@dataclass(frozen=True, slots=True)— стандарт для иммутабельных команд и Value Objects.StrEnumвместо строковых литералов для любого закрытого набора значений домена.type Alias = ...(синтаксис 3.12) — для сложных составных типов; неTypeAliasизtyping.@override(3.12) — обязателен на каждом переопределённом методе; ловит рассинхронизацию с базовым классом.- Walrus (
:=) — только когда устраняет буквальное дублирование и код становится яснее.frozen=Trueбезslots=Trueработает, но сslots=Trueэкономит память и ускоряет атрибутный доступ.PY-RUFF-4: семантикуPY-8.*ловитucp-py-style-review, не ruff/mypy.
Python 3.12 добавил синтаксис type, уточнил match/case (появившийся в 3.10), зафиксировал @override. Правила PY-8.* описывают, что из этого набора становится обязательным стилем, а не экзотикой.
match / case — структурный разбор
PY-8.1: match/case для разбора структур и sealed-подобных иерархий.
Длинная isinstance-цепочка сигналит об отсутствии match:
from dataclasses import dataclass
from decimal import Decimal
from uuid import UUID
@dataclass(frozen=True, slots=True)
class PlaceOrder:
order_id: UUID
amount: Decimal
@dataclass(frozen=True, slots=True)
class CancelOrder:
order_id: UUID
reason: str
@dataclass(frozen=True, slots=True)
class RefundOrder:
order_id: UUID
amount: Decimal
type OrderCommand = PlaceOrder | CancelOrder | RefundOrder
def handle(command: OrderCommand) -> None:
match command:
case PlaceOrder(order_id=oid, amount=amount):
_place(oid, amount)
case CancelOrder(order_id=oid, reason=reason):
_cancel(oid, reason)
case RefundOrder(order_id=oid, amount=amount):
_refund(oid, amount)
Деструктуризация в паттерне (order_id=oid) сразу извлекает поля — не нужны промежуточные .-обращения внутри ветки.
match читается как спецификация случаев: структура команды видна в заголовке ветки, а не зарыта в тело if.
Гвардии в паттерне
def apply_discount(order: PlaceOrder) -> Decimal:
match order:
case PlaceOrder(amount=amount) if amount > Decimal("10000"):
return amount * Decimal("0.05")
case PlaceOrder(amount=amount):
return Decimal("0")
Guard (if amount > ...) — часть паттерна, не отдельный if внутри ветки.
@dataclass(frozen=True, slots=True) — иммутабельные носители
PY-8.2: @dataclass(frozen=True, slots=True) для команд, запросов, Value Objects.
from dataclasses import dataclass
from decimal import Decimal
from uuid import UUID
@dataclass(frozen=True, slots=True)
class Money:
amount: Decimal
currency: str
def add(self, other: "Money") -> "Money":
if self.currency != other.currency:
raise ValueError(f"currency mismatch: {self.currency} vs {other.currency}")
return Money(amount=self.amount + other.amount, currency=self.currency)
@dataclass(frozen=True, slots=True)
class CreateOrder:
order_id: UUID
customer_id: UUID
total: Money
frozen=True запрещает мутацию атрибутов после создания — объект становится hashable и безопасен для передачи между слоями.
slots=True генерирует __slots__, устраняет __dict__ на экземпляре. Для carrier-объектов, создаваемых тысячами (команды, события), это снижает потребление памяти и ускоряет доступ.
Без slots=True можно навесить произвольный атрибут через instance.__dict__ — frozen этого не запрещает, только блокирует присваивание существующих полей через обычный синтаксис. Совместно frozen + slots делает объект действительно неизменяемым.
StrEnum — закрытые наборы значений
PY-8.3: enum.StrEnum для любого закрытого домена вместо строковых констант.
from enum import StrEnum
class OrderStatus(StrEnum):
PENDING = "PENDING"
CONFIRMED = "CONFIRMED"
SHIPPED = "SHIPPED"
DELIVERED = "DELIVERED"
CANCELLED = "CANCELLED"
class ProductCategory(StrEnum):
ELECTRONICS = "ELECTRONICS"
CLOTHING = "CLOTHING"
FOOD = "FOOD"
StrEnum — подкласс str, значение сравнивается как строка напрямую (order.status == "PENDING" работает), но mypy и IDE знают тип. При сериализации Pydantic и FastAPI отдают строку без дополнительных value-вызовов.
Enum (не StrEnum) подходит, если значения не строки: числа, объекты, кортежи.
Строковые литералы в коде — сигнал отсутствия enum:
if order.status == "PENDING":
...
Такой код не проверяется mypy на опечатки, не поддерживает автодополнение, рассыпается при переименовании значения.
type Alias — именованные составные типы
PY-8.4: оператор type (синтаксис 3.12) для сложных аннотаций.
from uuid import UUID
type CustomerId = UUID
type OrderId = UUID
type ProductId = UUID
type OrderIndex = dict[OrderId, list[ProductId]]
type CustomerOrders = dict[CustomerId, list[OrderId]]
До 3.12 нужно было TypeAlias из typing с присваиванием. Теперь type Name = ... — отдельный оператор, mypy и pyright понимают его нативно.
Правило применять, когда тип встречается в нескольких сигнатурах или сам по себе достаточно сложен (dict[str, list[tuple[int, Decimal]]]), чтобы его чтение замедляло понимание сигнатуры.
@override — явное переопределение
PY-8.4: @override (3.12, модуль typing) на каждом методе, переопределяющем базовый.
from typing import override
from app.ports.order_port import OrderPort
from app.domain.order import Order
from uuid import UUID
class PostgresOrderRepository(OrderPort):
@override
async def save(self, order: Order) -> None:
...
@override
async def find_by_id(self, order_id: UUID) -> Order | None:
...
@override — контракт: mypy проверяет, что метод с таким именем и совместимой сигнатурой существует в базовом классе. Если базовый класс переименовал метод — mypy укажет на все переопределения, которые стали «висящими».
Без @override переопределение выглядит как новый метод: лишняя нагрузка при ревью, риск молча создать метод-дублёр вместо переопределения.
Walrus-оператор := — осторожно
PY-8.5: walrus только когда устраняет дублирование и читаемость не страдает.
import re
LOG_PATTERN = re.compile(r"ERROR (?P<code>\d+): (?P<msg>.+)")
def extract_error(line: str) -> str | None:
if m := LOG_PATTERN.match(line):
return f"[{m.group('code')}] {m.group('msg')}"
return None
Здесь walrus оправдан: без него нужно либо два вызова match, либо промежуточная переменная m = ...; if m:.
Walrus не применяется ради краткости там, где промежуточная переменная яснее:
result := compute_heavy(order)
Если нет дублирования — обычное присваивание и явная проверка читаемее.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
isinstance-цепочка вместо match/case | PY-8.1 | match command: case PlaceOrder(...): |
@dataclass без frozen=True для VO/команды | PY-8.2 | @dataclass(frozen=True, slots=True) |
Строковые константы вместо StrEnum | PY-8.3 | class OrderStatus(StrEnum) |
TypeAlias из typing вместо type | PY-8.4 | type Alias = ... (3.12) |
Переопределение метода без @override | PY-8.4 | добавить @override перед методом |
| Walrus ради краткости без устранения дублирования | PY-8.5 | явное присваивание + отдельная проверка |
@dataclass(frozen=True) без slots=True для carrier'ов | PY-8.2 | добавить slots=True |
Куда дальше
- python/type-hints.md —
Protocol,X | None,Decimalиdatetimeaware. - python/expressions.md — guard clause, comprehension, EAFP/LBYL.
- python/naming.md —
StrEnum-значения и именование доменных типов. - /standards/backend/error-handling/ — обработка ошибок с
matchпо типу исключения.