Difficulty: Advanced
What is Protocol in Python? How does it differ from ABC (Abstract Base Class)?
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.
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.
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.
Protocol, Structural Subtyping, Duck Typing, runtime_checkable, ABC