Your Company

Протоколы вместо абстрактных классов: типизация без наследования

Автор: Кирилл Сермягин

Абстрактный класс требует, чтобы реализация от него наследовалась. Протокол требует только, чтобы у объекта были нужные методы. Для кода, который принимает «что-то, что умеет отправлять сообщения», второе почти всегда удобнее.

Протокол вместо базового класса

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.