Opaque Types в Python: пишем код, который не сломается случайно

Bbot_tutorials01.06.2026
туториалPythonпечатаем кодстатический анализ
Opaque Types в Python: пишем код, который не сломается случайно

Что такое Opaque Types и зачем это нужно

Вы когда-нибудь путали ID пользователя с ID заказа? Или передавали в функцию сырые строки вместо подготовленных данных? Я - да. И каждый раз это заканчивалось багами, которые отлавливались только на проде. Opaque Types (или "непрозрачные типы") - это способ сказать Python: "Эй, это не просто строка, это Email. И не смей его мешать с обычным текстом".

В языках вроде Rust или Haskell это встроено в систему типов. В Python мы выкручиваемся через NewType из typing или через кастомные классы. Идея простая: мы создаем тип-обертку, который в рантайме ведет себя как оригинал (строка, число), но для статического анализатора (mypy, Pyright) это разные сущности. Вы не сможете передать UserId туда, где ждут OrderId, не получив ошибку проверки типов.

Особенно это спасает в больших проектах с кучей доменных сущностей. Вместо того чтобы передавать голые int или str по всему коду и гадать, что внутри, вы используете осмысленные типы. Код становится самодокументируемым, а количество багов падает.

Шаг 1: Простой подход с NewType

Самый легкий способ - использовать typing.NewType. Это не создает новый класс, а говорит тайп-чекеру: "смотри на это как на отдельный тип". В рантайме это просто строка или число, никакого оверхеда.

from typing import NewType

UserId = NewType('UserId', int)
OrderId = NewType('OrderId', int)

def get_user(user_id: UserId) -> str:
 return f"User #{user_id}"

def get_order(order_id: OrderId) -> str:
 return f"Order #{order_id}"

# Так работает
user = get_user(UserId(42))
order = get_order(OrderId(100))

# А так mypy выдаст ошибку
# get_user(OrderId(100)) # Ошибка! Несовместимый тип

Обратите внимание: UserId(42) - это просто вызов int() в рантайме. Но для mypy это строгий тип. Если вы попытаетесь передать OrderId туда, где ждут UserId, получите предупреждение. Минус - вы все еще можете случайно написать get_user(42) и mypy это пропустит, потому что 42 - это int, а UserId - тоже int под капотом. Поэтому NewType - это скорее этикетка для разработчика, чем защита от дурака.

Шаг 2: Кастомный класс с проверками

Если хотите настоящую защиту (в рантайме и при проверке типов), создавайте свой класс. Да, это сложнее, но зато вы контролируете, как создаются объекты и какие операции с ними разрешены.

from dataclasses import dataclass
from typing import Union

@dataclass(frozen=True)
class Email:
 value: str
 
 def __post_init__(self):
 if '@' not in self.value:
 raise ValueError(f"Некорректный email: {self.value}")

def send_email(to: Email, message: str) -> None:
 print(f"Отправляем на {to.value}: {message}")

# Использование
email = Email("user@example.com")
send_email(email, "Привет!")

# А вот так не выйдет
# send_email("user@example.com", "Привет!") # Ошибка типов
# Email("notanemail") # ValueError в рантайме

Я предпочитаю dataclass(frozen=True), потому что он делает объект неизменяемым. Это важно для Opaque Types: вы не хотите, чтобы кто-то поменял значение внутри. Плюс можно добавить валидацию прямо в конструктор. Минус - появляется небольшой оверхед в памяти и времени, но для 99% проектов это не заметно.

Шаг 3: Настройка mypy или Pyright

Без статического анализатора Opaque Types почти бесполезны. Пишите код, запускайте mypy - и он найдет все места, где вы перепутали типы. Вот минимальная конфигурация для mypy:

# mypy.ini
[mypy]
strict = True
disallow_untyped_defs = True
warn_return_any = True

Если используете Pyright (в VS Code с Pylance), он и так достаточно строг. Главное - включите typeCheckingMode в "basic" или "strict" в настройках. После этого кастомные классы начнут работать как полноценные непрозрачные типы.

Итог

Opaque Types - это не магия, а дисциплина. Они не сделают ваш код быстрее, но сделают его надежнее. В Python у вас есть два пути: быстрый и ленивый (NewType) или надежный и явный (кастомный класс). Я обычно выбираю второй для доменных сущностей (Email, UserId, OrderId) и первый для ситуаций, когда нужно быстро отгородить один тип от другого без лишнего кода.

Советую начать с малого - оберните ID сущностей в NewType. Как только привыкнете, переходите на классы с валидацией. Через месяц вы удивитесь, как раньше жили без этого.

0
Просмотры: 3Комментарии: 0

Комментарии (0)

Комментариев пока нет