Protocol and Structural Subtyping

Difficulty: Advanced

Question

What is Protocol in Python? How does it differ from ABC (Abstract Base Class)?

Answer

Protocol (typing.Protocol, Python 3.8+) enables structural subtyping - a class satisfies a Protocol if it has the required methods/attributes, regardless of inheritance.

This formalizes Python's duck typing: 'if it walks like a duck and quacks like a duck, it is a duck'.

ABC vs Protocol: - ABC: nominal subtyping - must explicitly inherit (class MyClass(MyABC)) - Protocol: structural subtyping - must have the right methods (no inheritance needed)

Use Protocol when: working with third-party classes you cannot modify, keeping coupling low, multiple unrelated classes share behavior.

Code examples

Protocol vs ABC

from typing import Protocol, runtime_checkable
from abc import ABC, abstractmethod

# ABC approach - must inherit
class Drawable(ABC):
    @abstractmethod
    def draw(self) -> None: ...

# Protocol approach - no inheritance needed
@runtime_checkable
class Drawable(Protocol):
    def draw(self) -> None: ...

class Circle:
    def draw(self):
        print('Drawing circle')

class Square:
    def draw(self):
        print('Drawing square')

# Third-party class - you cannot modify it
class ThirdPartyWidget:
    def draw(self):
        print('Third party drawing')

def render(item: Drawable):
    item.draw()

# All work - no inheritance required
render(Circle())
render(Square())
render(ThirdPartyWidget())

# runtime_checkable enables isinstance
print(isinstance(Circle(), Drawable))  # True
print(isinstance(42, Drawable))        # False

Protocol enables duck typing with type checker support. ThirdPartyWidget satisfies Drawable without inheriting from it.

Complex Protocol

from typing import Protocol, Iterator

class Sequence(Protocol):
    def __len__(self) -> int: ...
    def __getitem__(self, index: int): ...
    def __iter__(self) -> Iterator: ...

class Stack:
    def __init__(self):
        self._items = []

    def push(self, item): self._items.append(item)
    def pop(self): return self._items.pop()

    # Satisfies Sequence protocol
    def __len__(self): return len(self._items)
    def __getitem__(self, i): return self._items[i]
    def __iter__(self): return iter(self._items)

def process(seq: Sequence):
    print(f'Length: {len(seq)}')
    for item in seq:
        print(item)

stack = Stack()
stack.push(1)
stack.push(2)
process(stack)  # Works: Stack satisfies Sequence

# Protocols can compose
class ReadableWritable(Protocol):
    def read(self) -> str: ...
    def write(self, data: str) -> None: ...

Stack satisfies Sequence without inheriting from it. Protocols can define any combination of methods and attributes.

Key points

Concepts covered

Protocol, Structural Subtyping, Duck Typing, runtime_checkable, ABC