Что такое Protocol в typing?
Protocol (typing) — structural typing: type checker считает класс совместимым с Protocol, если он реализует те же методы и атрибуты, без наследования. Это статический duck-typing. @runtime_checkable добавляет isinstance-проверку, но проверяет только наличие имён, не сигнатуры.
Что это и зачем
typing.Protocol (PEP 544, Python 3.8+) реализует structural subtyping: тип совместим с Protocol, если у него есть все нужные атрибуты и методы, независимо от того, наследуется ли он от Protocol-класса.
from typing import Protocol
class Sender(Protocol):
def send(self, message: str) -> None: ...
class ConsoleSender: # не наследуется от Sender
def send(self, message: str) -> None:
print(message)
class EmailSender:
def send(self, message: str) -> None:
smtp.sendmail(...)
def notify(sender: Sender, msg: str) -> None:
sender.send(msg)
notify(ConsoleSender(), "hi") # ok для mypy/pyright
notify(EmailSender(), "hi") # тоже ok
Protocol vs ABC
- ABC (
abc.ABC) — nominal subtyping: класс должен наследоваться от ABC. Подходит, когда вы владеете кодом и хотите явный контракт. - Protocol — structural: подходит любой класс с нужными методами. Идеально для интеграции со сторонним кодом и dependency injection, когда нельзя/неудобно править иерархию.
- Protocol можно использовать как тип параметра, ABC — и как тип, и как базу для миксинов.
@runtime_checkable
По умолчанию Protocol — статический. Чтобы работал isinstance(obj, Sender), добавьте декоратор:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Sender(Protocol):
def send(self, message: str) -> None: ...
isinstance(ConsoleSender(), Sender) # True
Важно: runtime-проверка смотрит только на наличие методов с правильными именами, не на их сигнатуры. Объект с send(self) без аргументов пройдёт.
Generic Protocol
from typing import Protocol, TypeVar
T = TypeVar("T", covariant=True)
class Reader(Protocol[T]):
def read(self) -> T: ...
class IntReader:
def read(self) -> int:
return 42
def consume(r: Reader[int]) -> int:
return r.read() + 1
consume(IntReader()) # ok
Атрибуты в Protocol
class Named(Protocol):
name: str # обязателен как атрибут
class User:
def __init__(self):
self.name = "Ada"
def greet(x: Named) -> str:
return f"Hello, {x.name}"
greet(User()) # ok
Если атрибут пишется через __init__ — runtime isinstance с @runtime_checkable может не увидеть его до создания инстанса.
Где Protocol полезен в реальной жизни
- Dependency injection в FastAPI/тестах: зависимость объявлена как Protocol, в production подставляется реальный клиент, в тестах — fake.
- Plugin-системы: сторонние модули реализуют контракт без импорта вашей библиотеки.
- Совместимость со сторонним кодом: sqlalchemy, requests — нельзя поменять иерархию, но можно описать нужную "форму".
- file-like / db-like объекты: вместо
IOопишите свойSupportsRead.
Подводные камни
isinstance(obj, Protocol)без@runtime_checkable—TypeError.- Runtime-проверка игнорирует сигнатуры и аннотации параметров — pyright/mypy здесь строже.
- Protocol с большим количеством методов — нарушение ISP, лучше разбить.
- Унаследовать конкретный класс от Protocol — он становится «обязательным» (методы должны быть реализованы), и теряется смысл «structural».
- В pydantic v1 поля + Protocol работали плохо; v2 лучше.
- Не все методы Protocol должны быть
...— можно дать default implementation, но осторожно с диамондом. - На 3.7 и ниже Protocol требовал
typing_extensions; на 3.8+ — в stdlib.
Common mistakes
- Не объяснять structural typing.
- Использовать ABC там, где Protocol проще.
- Писать огромные Protocol с десятками методов.
What the interviewer is testing
- Понимает static duck typing.
- Может показать dependency injection.
- Отличает Protocol от ABC.