Python ABCs and Protocols, explained
Python is famous for duck typing — "if it walks like a duck...". So why does it also have
abstract base classes and Protocol? Because sometimes you want to declare and enforce
an interface. This guide covers the three ways to express "must support these methods".
Duck typing — the baseline
By default, Python doesn't check types; it checks capabilities at runtime. If an object has the method you call, it works:
def make_it_quack(thing):
thing.quack() # works for ANY object that has .quack()
class Duck:
def quack(self): print("Quack")
class Person:
def quack(self): print("I'm quacking")
make_it_quack(Duck()) # fine
make_it_quack(Person()) # also fine — no shared base needed
This is flexible but offers no early guarantee — the failure (AttributeError) only shows up
when the missing method is actually called.
Abstract base classes
An ABC defines an interface that subclasses must implement. Mark required methods with
@abstractmethod, and the class can't be instantiated until they're all provided:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
...
Shape() # TypeError — can't instantiate abstract class
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self): # must implement, or Circle is abstract too
return 3.14159 * self.r ** 2
Circle(2).area() # 12.566...
This turns "you forgot to implement area" from a runtime surprise into an error at object
creation.
Why use ABCs over plain classes
ABCs give you three things plain classes don't: enforcement (can't instantiate without
the methods), documentation (the interface is explicit), and isinstance grouping
(register virtual subclasses, and isinstance(obj, Shape) reflects the contract). Use them
when you have a real family of types that must share an interface.
collections.abc
The standard library ships ready-made ABCs for the container protocols in
collections.abc — Iterable, Iterator, Sequence, Mapping, Hashable, and more. You
can inherit from them to get mixin methods for free, or use them in isinstance checks:
from collections.abc import Iterable, Mapping
isinstance([1, 2], Iterable) # True
isinstance({}, Mapping) # True
class MyList(Sequence): # implement __getitem__ + __len__...
... # ...and get __contains__, __iter__, etc. free
typing.Protocol — structural typing
ABCs require explicit inheritance (nominal typing). typing.Protocol instead checks
structure: any class with the right methods satisfies the protocol, with no
inheritance needed — duck typing that a static type checker can verify:
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> str: ...
def render(item: Drawable) -> str:
return item.draw()
class Button: # does NOT inherit Drawable
def draw(self) -> str:
return "[Button]"
render(Button()) # type checker accepts it — it has draw()
This gives you the flexibility of duck typing plus static safety from mypy/pyright.
ABCs vs Protocols — which to choose
- Use an ABC when you own the hierarchy and want runtime enforcement and shared mixin behaviour through inheritance.
- Use a Protocol when you want to type-check third-party or unrelated classes by shape, without forcing them to inherit anything.
Add @runtime_checkable to a Protocol if you also need isinstance checks against it (it
then verifies method presence, not signatures).
Recap
Duck typing is Python's default — capabilities are checked at call time, flexible but
late-failing. Abstract base classes (ABC + @abstractmethod) declare and enforce an
interface, blocking instantiation until it's implemented, and collections.abc provides
ready-made container ABCs. typing.Protocol brings structural typing: classes match
by having the right methods, no inheritance required, giving duck typing that static checkers
can verify. Choose ABCs for owned hierarchies with runtime enforcement, Protocols for typing
unrelated classes by shape.