Опирается на правила:
PY-6.1…PY-6.4,PY-6.X1,PY-6.X2,PY-RUFF-2,PY-RUFF-3из Python Style Guide → раздел 6. Тайп-хинты и mypy.
Важно знать
- Все публичные сигнатуры (функции, методы, атрибуты
dataclass) аннотированы;mypy --strictзелёный в CI.X | NoneвместоOptional[X];list[X],dict[K, V]вместоList/Dictизtyping(Python 3.10+).Protocol— для структурной типизации портов/интерфейсов;ABC— когда нужна реализация или иерархия с общим поведением.- Деньги —
Decimal, никогдаfloat; дата-время — awaredatetime(сtzinfo), не naive.Anyкак заглушка для mypy — нарушениеPY-6.X1; разрешён только с кодом правила и обоснованием.# type: ignore— только с кодом (# type: ignore[assignment]) и комментарием WHY.mypy --strictослабляется исключительно через per-module override вpyproject.tomlс обоснованием (PY-RUFF-2).ruff check+ruff format --check+mypy— тройной прогон на каждом CI-прогоне, fail при нарушении.
Аннотации типов в Python — не опциональный «сахар», а контракт, который mypy проверяет статически. Без --strict аннотации становятся документацией, а не гарантией: mypy молчит о неаннотированных функциях, и первый же Any-«пробел» разрушает всю цепочку выводимых типов. Именно поэтому PY-6.1 требует полного покрытия публичных сигнатур, а PY-RUFF-2 — --strict в CI.
Аннотации публичных сигнатур
PY-6.1: функции, методы, атрибуты dataclass — все аннотированы.
from decimal import Decimal
from uuid import UUID
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class CreateOrder:
customer_id: UUID
amount: Decimal
async def create_order(command: CreateOrder) -> UUID:
...
def find_by_id(order_id: UUID) -> "Order | None":
...
mypy --strict включает в себя --disallow-untyped-defs, --disallow-incomplete-defs, --warn-return-any и ещё восемь флагов. Написать «почти аннотированную» функцию с Any-параметром — то же самое, что не написать аннотацию вовсе: вывод типов прерывается.
Синтаксис 3.10+ — Union и встроенные дженерики
PY-6.2: новый синтаксис обязателен для кода, требующего Python 3.10+.
def find(order_id: UUID) -> Order | None: ...
def batch_update(order_ids: list[UUID], status: OrderStatus) -> dict[UUID, bool]: ...
def process(items: list[Order] | None = None) -> None:
items = items or []
...
Импорты Optional, List, Dict, Tuple, Union из typing заменяются встроенными аналогами:
from typing import Optional, List, Dict
def find(order_id: UUID) -> Optional[Order]: ...
items: List[Order]
cache: Dict[str, Order]
def find(order_id: UUID) -> Order | None: ...
items: list[Order]
cache: dict[str, Order]
Если проект поддерживает Python 3.9, используют from __future__ import annotations — тогда X | None работает без рантайм-ошибок и до 3.10.
Protocol для портов и структурной типизации
PY-6.3: Protocol — для описания интерфейсов, которые не требуют наследования.
from typing import Protocol
from uuid import UUID
from decimal import Decimal
class OrderRepository(Protocol):
async def save(self, order: "Order") -> None: ...
async def find_by_id(self, order_id: UUID) -> "Order | None": ...
class PaymentGateway(Protocol):
async def charge(self, order_id: UUID, amount: Decimal) -> str: ...
async def refund(self, transaction_id: str) -> None: ...
Protocol реализует структурную типизацию (duck typing): класс удовлетворяет Protocol, если у него есть нужные методы с совместимыми сигнатурами — наследоваться не нужно. Это ключевое отличие от ABC.
ABC уместен когда:
- порт предоставляет общую реализацию (template method);
- иерархия намеренно закрытая, и
isinstance-проверки нужны в рантайме.
from abc import ABC, abstractmethod
class BaseNotifier(ABC):
def notify(self, order_id: UUID, message: str) -> None:
self._log(order_id, message)
self._send(order_id, message)
@abstractmethod
def _send(self, order_id: UUID, message: str) -> None: ...
def _log(self, order_id: UUID, message: str) -> None:
...
В гексагональной архитектуре порты — всегда Protocol; ABC — только для базовых классов с реализацией внутри домена.
Деньги — Decimal, время — aware datetime
PY-6.4: две семантически нагруженные ошибки.
float для денег
price: float = 19.99
discount: float = 0.1
total = price - price * discount
float — бинарная дробь. 19.99 * 0.1 в Python даёт 1.9999999999999998, не 2.00. Для финансовых расчётов это неприемлемо.
from decimal import Decimal
price: Decimal = Decimal("19.99")
discount: Decimal = Decimal("0.1")
total: Decimal = price - price * discount
Инициализация через строку (Decimal("19.99")), не через float (Decimal(19.99) вносит ошибку ещё до Decimal).
naive datetime
from datetime import datetime
created_at: datetime = datetime.now()
datetime.now() возвращает naive datetime — без информации о часовом поясе. При сравнении, сериализации или передаче между сервисами naive datetime приводит к молчаливым ошибкам — особенно при переходе на летнее время.
from datetime import datetime, timezone
created_at: datetime = datetime.now(tz=timezone.utc)
expires_at: datetime = datetime.now(tz=timezone.utc)
mypy с --strict не различает naive и aware datetime — это семантическая ошибка вне статического анализа. PY-6.4 закрывает этот пробел конвенцией.
Any и type: ignore — только с обоснованием
PY-6.X1: Any как способ заглушить mypy — запрещён без явного обоснования.
from typing import Any
def process(data: Any) -> Any:
return data["result"]
Такая функция «выключает» mypy: всё, что через неё проходит, теряет тип. Каждый Any — дыра в типовой безопасности.
Допустимый случай — граница с нетипизированной библиотекой, где stubs отсутствуют:
from typing import Any
def parse_response(raw: Any) -> OrderResponse:
return OrderResponse.model_validate(raw)
# type: ignore — аналогично: только с кодом правила и комментарием (PY-RUFF-3):
result = legacy_client.fetch() # type: ignore[no-untyped-call] # legacy_client не имеет stubs
Без кода и обоснования # type: ignore нарушает PY-RUFF-3 и блокируется ruff check.
Полный пример — сигнатуры агрегата и порта
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from decimal import Decimal
from enum import StrEnum
from typing import Protocol
from uuid import UUID
class OrderStatus(StrEnum):
PENDING = "PENDING"
CONFIRMED = "CONFIRMED"
CANCELLED = "CANCELLED"
@dataclass(frozen=True, slots=True)
class OrderItem:
product_id: UUID
quantity: int
unit_price: Decimal
@dataclass(slots=True)
class Order:
id: UUID
customer_id: UUID
status: OrderStatus
items: list[OrderItem]
created_at: datetime = field(default_factory=lambda: datetime.now(tz=timezone.utc))
total_amount: Decimal = field(init=False)
def __post_init__(self) -> None:
object.__setattr__(
self,
"total_amount",
sum(i.unit_price * i.quantity for i in self.items),
)
def confirm(self) -> Order:
...
def cancel(self, reason: str) -> Order:
...
class OrderRepository(Protocol):
async def save(self, order: Order) -> None: ...
async def find_by_id(self, order_id: UUID) -> Order | None: ...
async def find_by_customer(
self,
customer_id: UUID,
status: OrderStatus | None = None,
) -> list[Order]: ...
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
def find(order_id) -> Optional[Order] | PY-6.1, PY-6.2 | def find(order_id: UUID) -> Order \| None |
from typing import List, Dict, Optional | PY-6.2 | встроенные list, dict, X \| None |
price: float = 19.99 для денег | PY-6.4 | price: Decimal = Decimal("19.99") |
datetime.now() без tz | PY-6.4 | datetime.now(tz=timezone.utc) |
class OrderRepo(ABC): ... для порта без реализации | PY-6.3 | class OrderRepo(Protocol): ... |
def process(data: Any) -> Any без обоснования | PY-6.X1 | конкретный тип или TypeVar |
# type: ignore без кода правила | PY-6.X1, PY-RUFF-3 | # type: ignore[code] # justify: ... |
| Аннотация расходится с реальным типом | PY-6.X2 | аннотация отражает реальный тип |
Куда дальше
- Именование —
PascalCase-классы,snake_case-функции,ProtocolбезI-префикса. - Импорты — абсолютные импорты, группировка stdlib/third-party/local, запрет wildcard.
- Современный Python —
@dataclass(frozen=True, slots=True),StrEnum,type Alias =(3.12),@override. - Enforcement ruff + mypy — конфиг
pyproject.toml, per-module overrides,# noqaс justify. - Hexagonal — Python —
Protocolкак порт в гексагональной архитектуре, структурная типизация адаптеров.