Protocol в Python: структурная типизация и утиная типизация

21 июля 2026
Автор

Антон Ларичев

Что такое Protocol и зачем он нужен

Python изначально строился на принципе утиной типизации: если объект ведёт себя как утка — крякает и ходит вразвалку — значит, это утка. Тип объекта не важен, важно наличие нужных методов и атрибутов.

Долгое время это делало статическую проверку типов крайне затруднительной. isinstance() требует явного наследования, а mypy не понимал, что объект «подходит» под интерфейс просто потому, что у него есть нужные методы.

Protocol — это решение, появившееся в Python 3.8 (PEP 544). Он позволяет описать интерфейс структурно: объект считается совместимым с протоколом, если у него есть все необходимые методы и атрибуты — без явного наследования от протокола.

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None:
        ...

class Circle:
    def draw(self) -> None:
        print("Drawing circle")

class Square:
    def draw(self) -> None:
        print("Drawing square")

def render(shape: Drawable) -> None:
    shape.draw()

render(Circle())  # OK
render(Square())  # OK

Ни Circle, ни Square не наследуются от Drawable — но оба совместимы с ним структурно. mypy это понимает и не выдаёт ошибок.

Курс по теме

Курс Python с нуля

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Protocol vs ABC: в чём разница

До появления Protocol для описания интерфейсов использовались абстрактные базовые классы из модуля abc.

from abc import ABC, abstractmethod

class DrawableABC(ABC):
    @abstractmethod
    def draw(self) -> None:
        ...

class Triangle(DrawableABC):
    def draw(self) -> None:
        print("Drawing triangle")

# Этот класс НЕ совместим с DrawableABC без явного наследования
class Pentagon:
    def draw(self) -> None:
        print("Drawing pentagon")

Если передать Pentagon туда, где ожидается DrawableABC, mypy выдаст ошибку — несмотря на наличие метода draw. С Protocol такой проблемы нет.

Сравнение двух подходов:

Критерий ABC Protocol
Требует наследования Да Нет
Структурная совместимость Нет Да
Проверка через isinstance Да Только с @runtime_checkable
Подходит для сторонних классов Нет Да

ABC хорошо подходит, когда вы контролируете все реализации и хотите принудить к явному наследованию. Protocol лучше подходит для описания интерфейсов, которым должны соответствовать классы из внешних библиотек или классы, которые уже существуют.

Базовый синтаксис Protocol

Протокол определяется как класс, наследующий от Protocol. Тело протокола содержит сигнатуры методов и объявления атрибутов.

from typing import Protocol

class Serializable(Protocol):
    def to_json(self) -> str:
        ...
    
    def to_dict(self) -> dict:
        ...

class User:
    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age
    
    def to_json(self) -> str:
        import json
        return json.dumps(self.to_dict())
    
    def to_dict(self) -> dict:
        return {"name": self.name, "age": self.age}

def save(obj: Serializable) -> None:
    data = obj.to_json()
    print(f"Saving: {data}")

save(User("Alice", 30))  # OK — mypy не выдаст ошибку

Обратите внимание на ... в теле методов протокола. Это не абстрактные методы — в протоколе можно даже предоставить реализацию по умолчанию, хотя чаще тело методов остаётся пустым.

Атрибуты в Protocol

Протокол может описывать не только методы, но и атрибуты:

from typing import Protocol

class HasName(Protocol):
    name: str

class HasNameAndAge(Protocol):
    name: str
    age: int

class Employee:
    def __init__(self, name: str, age: int, department: str) -> None:
        self.name = name
        self.age = age
        self.department = department

def greet(entity: HasName) -> str:
    return f"Hello, {entity.name}!"

greet(Employee("Bob", 25, "Engineering"))  # OK

Для атрибутов только для чтения используется ClassVar или дескрипторы. Если атрибут должен быть только читаемым (не записываемым), используйте @property в протоколе:

from typing import Protocol

class ReadOnlyName(Protocol):
    @property
    def name(self) -> str:
        ...

class ImmutableUser:
    def __init__(self, name: str) -> None:
        self._name = name
    
    @property
    def name(self) -> str:
        return self._name

runtime_checkable: проверка во время выполнения

По умолчанию Protocol работает только на уровне статической проверки типов. Вызов isinstance(obj, MyProtocol) вызовет TypeError. Чтобы разрешить проверку в рантайме, используйте декоратор @runtime_checkable:

from typing import Protocol, runtime_checkable

@runtime_checkable
class Drawable(Protocol):
    def draw(self) -> None:
        ...

class Circle:
    def draw(self) -> None:
        print("Circle")

class Point:
    pass

print(isinstance(Circle(), Drawable))  # True
print(isinstance(Point(), Drawable))   # False
print(isinstance("hello", Drawable))   # False

Важно понимать ограничение: isinstance с runtime_checkable проверяет только наличие методов и атрибутов, но не их сигнатуры. Если у класса есть метод draw, но с другой сигнатурой — проверка всё равно пройдёт успешно.

class FakeDrawable:
    def draw(self, x: int, y: int) -> str:  # Другая сигнатура
        return "wrong"

print(isinstance(FakeDrawable(), Drawable))  # True — только проверяет наличие метода

Поэтому @runtime_checkable подходит для базовых проверок, но полную гарантию совместимости даёт только статический анализатор.

Наследование протоколов

Протоколы можно комбинировать через наследование, создавая составные интерфейсы:

from typing import Protocol

class Readable(Protocol):
    def read(self) -> str:
        ...

class Writable(Protocol):
    def write(self, data: str) -> None:
        ...

class ReadWritable(Readable, Writable, Protocol):
    pass

class FileBuffer:
    def __init__(self) -> None:
        self._data = ""
    
    def read(self) -> str:
        return self._data
    
    def write(self, data: str) -> None:
        self._data += data

def process(stream: ReadWritable) -> None:
    stream.write("hello")
    print(stream.read())

process(FileBuffer())  # OK

Обратите внимание: при создании составного протокола через наследование нужно явно указать Protocol в списке базовых классов, иначе mypy не распознает его как протокол.

Generic Protocol: параметрический протокол

Протоколы поддерживают обобщённые типы через Generic:

from typing import Protocol, TypeVar

T = TypeVar("T")

class Repository(Protocol[T]):
    def get(self, id: int) -> T:
        ...
    
    def save(self, entity: T) -> None:
        ...
    
    def delete(self, id: int) -> None:
        ...

class User:
    def __init__(self, id: int, name: str) -> None:
        self.id = id
        self.name = name

class UserRepository:
    def __init__(self) -> None:
        self._storage: dict[int, User] = {}
    
    def get(self, id: int) -> User:
        return self._storage[id]
    
    def save(self, entity: User) -> None:
        self._storage[entity.id] = entity
    
    def delete(self, id: int) -> None:
        del self._storage[id]

def find_and_print(repo: Repository[User], user_id: int) -> None:
    user = repo.get(user_id)
    print(user.name)

repo = UserRepository()
repo.save(User(1, "Alice"))
find_and_print(repo, 1)  # Выведет: Alice

Практический пример: плагинная система

Protocol отлично подходит для реализации плагинных архитектур, где основной код не должен зависеть от конкретных реализаций:

from typing import Protocol

class NotificationSender(Protocol):
    def send(self, recipient: str, message: str) -> bool:
        ...
    
    def supports_bulk(self) -> bool:
        ...

class EmailSender:
    def send(self, recipient: str, message: str) -> bool:
        print(f"Email to {recipient}: {message}")
        return True
    
    def supports_bulk(self) -> bool:
        return True

class SmsSender:
    def send(self, recipient: str, message: str) -> bool:
        if len(message) > 160:
            return False
        print(f"SMS to {recipient}: {message}")
        return True
    
    def supports_bulk(self) -> bool:
        return False

class TelegramSender:
    def send(self, recipient: str, message: str) -> bool:
        print(f"Telegram to @{recipient}: {message}")
        return True
    
    def supports_bulk(self) -> bool:
        return True

class NotificationService:
    def __init__(self, sender: NotificationSender) -> None:
        self._sender = sender
    
    def notify(self, recipient: str, message: str) -> None:
        success = self._sender.send(recipient, message)
        if not success:
            print(f"Failed to send notification to {recipient}")

# Все три класса совместимы с NotificationSender без наследования
service = NotificationService(EmailSender())
service.notify("user@example.com", "Welcome!")

service = NotificationService(SmsSender())
service.notify("+79001234567", "Code: 1234")

Это позволяет легко добавлять новые реализации без изменения NotificationService.

Практический пример: стратегия сортировки

from typing import Protocol, TypeVar

T = TypeVar("T")

class Comparable(Protocol):
    def __lt__(self, other: object) -> bool:
        ...
    
    def __le__(self, other: object) -> bool:
        ...

class SortStrategy(Protocol[T]):
    def sort(self, data: list[T]) -> list[T]:
        ...

class BubbleSort:
    def sort(self, data: list[int]) -> list[int]:
        result = data.copy()
        n = len(result)
        for i in range(n):
            for j in range(n - i - 1):
                if result[j] > result[j + 1]:
                    result[j], result[j + 1] = result[j + 1], result[j]
        return result

class QuickSort:
    def sort(self, data: list[int]) -> list[int]:
        if len(data) <= 1:
            return data
        pivot = data[len(data) // 2]
        left = [x for x in data if x < pivot]
        middle = [x for x in data if x == pivot]
        right = [x for x in data if x > pivot]
        return self.sort(left) + middle + self.sort(right)

class Sorter:
    def __init__(self, strategy: SortStrategy[int]) -> None:
        self._strategy = strategy
    
    def sort(self, data: list[int]) -> list[int]:
        return self._strategy.sort(data)

data = [5, 2, 8, 1, 9, 3]

sorter = Sorter(BubbleSort())
print(sorter.sort(data))  # [1, 2, 3, 5, 8, 9]

sorter = Sorter(QuickSort())
print(sorter.sort(data))  # [1, 2, 3, 5, 8, 9]

Совместимость с Protocol из typing_extensions

Если вы поддерживаете Python ниже 3.8, используйте typing_extensions:

try:
    from typing import Protocol, runtime_checkable
except ImportError:
    from typing_extensions import Protocol, runtime_checkable  # type: ignore

Начиная с Python 3.12, Protocol также поддерживает синтаксис type для определения обобщённых типов:

# Python 3.12+
from typing import Protocol

class Container[T](Protocol):
    def get(self) -> T:
        ...
    
    def set(self, value: T) -> None:
        ...

Типичные ошибки при работе с Protocol

Ошибка 1: Забыть указать Protocol при составном протоколе

# Неправильно — mypy не распознает как Protocol
class ReadWritable(Readable, Writable):
    pass

# Правильно
class ReadWritable(Readable, Writable, Protocol):
    pass

Ошибка 2: Рассчитывать на полную проверку сигнатур в рантайме

@runtime_checkable
class Processable(Protocol):
    def process(self, data: str) -> int:
        ...

class Wrong:
    def process(self) -> None:  # Неверная сигнатура
        pass

# isinstance вернёт True — проверяется только наличие метода!
print(isinstance(Wrong(), Processable))  # True

Ошибка 3: Использовать Protocol как базовый класс для реализации

class MyProtocol(Protocol):
    def do_something(self) -> None:
        ...

# Это технически работает, но нарушает смысл Protocol
# Лучше использовать ABC, если нужно наследование
class MyClass(MyProtocol):  # Не рекомендуется
    def do_something(self) -> None:
        print("Done")

Когда использовать Protocol, а когда ABC

Используйте Protocol, когда:

  • Нужно типизировать существующие классы из внешних библиотек
  • Реализации не должны знать об интерфейсе (слабая связность)
  • Хотите описать «утиный» интерфейс для статического анализа
  • Работаете с несколькими несвязанными классами, которые случайно реализуют одни и те же методы

Используйте ABC, когда:

  • Нужно принудить к явному наследованию
  • Хотите предоставить общую базовую реализацию
  • Реализация должна явно объявлять себя частью иерархии типов
  • Нужна надёжная проверка через isinstance в рантайме

Для углублённого изучения Python, включая систему типов, ООП и современные практики разработки, смотрите курс на PurpleSchool: https://purpleschool.ru/course/python?utm_source=knowledgebase&utm_medium=text&utm_campaign=python-protocol

Python dataclasses: создание классов данныхСтрелочка вправо

Постройте личный план изучения Python до уровня Middle — бесплатно!

Python — часть карты развития Backend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Все гайды по Python

Как отправлять запросы с помощью requests в PythonПочему Python выводит значение без команды printКак работает команда print в PythonВозможности Python для автоматизации задачРабота с JSON в Python на примерахPython get — методы получения данныхКак находить и исправлять ошибки в PythonРабота с данными через API и внешние сервисыСтруктура и оформление кода PythonОсновы Django с PythonПолезные приёмы в Python для повседневной работыИспользование locals в Python для отладкиИнтеграция PHP и PythonКак выполнять HTTPS-запросы в PythonFastAPI Python — быстрый старт: создание REST API с нуляКак работать с API в Python
Ввод целого числа в PythonУдаление данных в Python с помощью removeВедение логов в PythonОбработка исключений с помощью try/except в PythonФункция super() в Python — как вызвать метод родителяСоздание собственных контекстных менеджеров в PythonРабота с символами программирования PythonРабота с переменной X в PythonРабота с классами в PythonКак скачать Python на компьютерПростая программа на Python для начинающихОсновы Python для тех, кто начинаетЧто нового в Python 3Поддерживается ли Python 2 и стоит ли его использоватьPython 1 — с чего начиналась история языкаКоманда python print - полное руководство по выводу данныхПравила именования переменных в PythonПользовательские исключения в PythonОсновы Python coreОписание объектов PythonНаследование классов в Python — основы и примерыМножественное наследование в Python — примеры и MROКонтекстный менеджер with в Python — как работает и зачем нуженКомментарии в Python — однострочные, многострочные и docstringКакой Python выбрать для установкиКак вывести целое число с помощью print в PythonКак установить Python на Windows macOS и LinuxКак пользоваться консолью PythonКак получить последний элемент в PythonКак найти значение в PythonКак настроить PythonКак использовать print для строк в PythonКак работает интерпретатор PythonИнструкция по работе с PythonБлок finally в обработке исключений PythonЦелые числа в PythonАбстрактные классы в Python — ABC и abstractmethod
Pydantic: валидация данных в Python
Загрузка данных PythonУправление проектами на GitHub с PythonСоздание веб-приложений на Flask PythonСоздание бота на PythonСоздание интерфейсов Python QTСоздание игр с PygameСоздание GUI в PythonКак работать со словарями в PythonРабота с библиотеками через Python PackagingРабота со временем в Python при помощи модуля timePython name — особенности переменнойМатематические операции в Python с модулем mathPython listing — что это и как использоватьPytest — тестирование на Python: полное руководствоОбработка изображений с OpenCV PythonNumPy в Python — основы и применение в задачахМашинное обучение с PythonИспользование Anaconda с PythonМодуль contextlib в Python — утилиты для контекстных менеджеровБиблиотеки Python и их применение в проектах
Возврат значений из функции в PythonВложенные функции в PythonСоздание собственных декораторов в PythonРабота с функцией map в PythonЦикл while в Python и примеры использованияОбработка чисел, введённых через input в PythonОсновные операторы в Python с примерамиУсловные выражения if else в Python для начинающихКак выполняется вызов функций call в PythonПродвинутые генераторы в Python — send, throw, close и корутиныПозиционные и именованные аргументы в PythonОбъявление переменных и управление областью видимости в PythonПередача аргументов по ссылке и по значению в PythonПередача аргументов через args и kwargs в PythonОсновные методы Python и примеры их использованияОператор match/case в Python 3.10+ — основы структурного сопоставленияПаттерны match/case в Python — деструктуризация, guard и вложенные шаблоныПрактические примеры match/case в Python — реальные сценарии примененияЛокальные и глобальные переменные в PythonЧасто используемые команды PythonКлючевые слова global и nonlocal в PythonКак создавать функции в PythonКак работает сборщик мусора в PythonКак работает область видимости переменных в PythonКак работает функция callable в PythonКак работает функция any и all в PythonКак проверить тип переменной в PythonКак передать функцию как аргумент в PythonКак использовать функцию isinstance в PythonКак использовать функцию filter в PythonКак использовать функцию filter в PythonКак использовать функцию eval безопасно в PythonКак использовать декораторы в PythonИзменяемые и неизменяемые типы данных в PythonГенераторы и yield в Python — как создавать и использоватьГенераторные выражения в Python — синтаксис и примерыФункции в Python и способы их вызоваФункции как объекты в PythonЧто такое замыкания в PythonЧто делает функция reduce в PythonЧто делает функция id в PythonАргументы по умолчанию в PythonАнонимные функции и lambda в PythonАлгоритмы на Python — примеры и объяснение
Запись данных в PythonУстановка pip в PythonУправление зависимостями requirement в PythonУправление библиотеками с помощью Python PackagingУдаление пробелов с помощью strip в PythonСтруктурирование кода в PythonСоздание исполняемого файла Python в exeРазбор traceback в модуле PythonРазбор site-packages в PythonРазбор Program Files в PythonРабота с Unicode кодировками в PythonРабота с системными функциями Python sysРабота с папкой AppData в PythonРабота с модулем logging в PythonРабота с каталогами в PythonРабота с CSV в PythonВиртуальная среда venv в Python — создание и настройкаКак создать простое приложение на PythonИспользование pip в Python для установки пакетовМодули в Python и организация кода в проектеИмпорт модулей в Python и правила подключенияРабота с файлами в Python пошаговоЧто делает компилятор Python и как он работаетПолучение строки из модуля PythonПодключение файлов в Python с includeПеременные среды в PythonСборка проекта с помощью packaging в PythonНастройка Python сервераИспользование Python на UbuntuИспользование консоли PythonИспользование кодировок в PythonИнициализация пакетов PythonИмпорт модулей PythonИмпорт имен в PythonСреда IDLE Python и базовые возможностиЧтение и запись TXT в PythonЧтение файлов в Python с помощью open file
Удаление элементов из списка PythonТипы данных в Python — обзор и рекомендацииОсновные операции со строками в PythonМетоды str в Python и обработка текстаСписки в Python и их ключевые методыСоздание списков данных в PythonРабота со строками и символами в PythonРабота со столбцами в PythonРабота со списком значений в PythonРабота с таблицами в Python с помощью DataFrameРабота с RFR в PythonРабота с пробелами в PythonРабота с массивами в PythonРабота с кортежами tuple PythonРабота с координатами X и Y в PythonРабота с ключами в PythonРабота с элементами данных PythonРабота с двоичными числами PythonРабота с данными в PythonРабота с данными NumPy PythonРабота с большими числами в PythonРабота с битами в PythonРабота с байтами в PythonЧто такое значение в Python и как его определитьМножества в Python и операции с нимиИспользование range в Python для цикловПроверка на четность в PythonПроверка числа в PythonПреобразование типов в PythonПреобразование списка в строку PythonПреобразование числа в строку в PythonПостроение графиков в PythonОпределение индекса элемента в PythonОкругление чисел в PythonОбъединение списков в Python с помощью zipМножества в PythonМассивы в Python и отличие от списковМассив чисел в PythonКортежи данных в PythonКак вычислить сумму чисел в PythonКак получить остаток от деления в PythonКак найти следующее число в PythonИспользование Unicode в PythonТип int в Python и его особенностиИндекс списка в PythonФункции для работы со строками в PythonЭлементы Python и способы доступа к нимДоступ к элементам массива в PythonДеление чисел в PythonРабота с данными в Python на практикеКак работать с числами в Python
Открыть базу знаний

Лучшие курсы по теме

Иконка молнииНовый
изображение курса

Основы Python

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий