Протоколы вместо абстрактных классов: типизация без наследования
Автор: Кирилл Сермягин
Абстрактный класс требует, чтобы реализация от него наследовалась. Протокол требует только, чтобы у объекта были нужные методы. Для кода, который принимает «что-то, что умеет отправлять сообщения», второе почти всегда удобнее.
Протокол вместо базового класса
from typing import Protocol
class Sender(Protocol):
def send(self, to: str, text: str) -> None: ...
class SmtpSender:
def send(self, to: str, text: str) -> None:
...
def notify(sender: Sender, users: list[User]) -> None:
for user in users:
sender.send(user.email, "Новый выпуск дайджеста")
notify(SmtpSender(), users) # без наследования — типы сходятся
SmtpSender ничего не знает о Sender, но проверка типов пройдёт: совпадает структура. В тестах подойдёт любой объект с методом send — без моков и наследования.
Проверка во время выполнения
Декоратор @runtime_checkable разрешает isinstance(obj, Sender), но проверяет только наличие методов, а не их сигнатуры.
Что скажут mypy и pyright
Оба анализатора проверяют протоколы одинаково строго, но по-разному сообщают об ошибках: pyright показывает, какой именно метод не совпал, mypy — только факт несовместимости. Подробное сравнение — в докладе «Типизация в 2026».
- Протокол — для зависимостей, которые передают в функцию.
- ABC — когда нужна общая реализация в базовом классе.
- Generic-протоколы с синтаксисом
class Repo[T](Protocol)— с Python 3.12.