Interfaces: From Protocols to Abstract Base Classes
Notes based on Fluent Python, summarized and rephrased in my own words.[file:18]
Interfaces and protocols in Python culture
Python does not have an interface keyword. Instead, every class has an interface, defined by its public attributes (methods and data attributes), including special methods like __getitem__ or __add__.[file:18]
- "Protected" attributes (single leading underscore) and "private" attributes (double leading underscore) are not part of the public interface by convention.[file:18]
- You can access them, but you generally should not; the convention is what matters.
A protocol is an interface defined only by documentation and convention. It is informal and not enforced by the language.[file:18]
- A class may implement some or all of a protocol.
- If it behaves as expected (e.g. supports
len()and__getitem__), we treat it as conforming, regardless of inheritance.
Abstract Base Classes (ABCs) are Python’s way to move from purely informal protocols toward optional, enforceable interfaces.[file:18]
Python loves sequences
The Python data model goes out of its way to support sequence-like behavior, even from minimal implementations.[file:18]
Example: a class implementing only __getitem__:
class Foo:
def __getitem__(self, pos):
return range(0, 30, 10)[pos]
f = Foo()
print(f[1]) # 10
for i in f:
print(i) # 0, 10, 20
print(20 in f) # True
print(15 in f) # FalseEven though Foo has no __iter__ or __contains__ methods, Python:
- Uses
__getitem__with increasing indices starting at 0 as a fallback for iteration.[file:18] - Uses iteration as a fallback for
in, so membership tests still work.[file:18]
Conclusion: for sequences, if __iter__ and __contains__ are missing, Python tries __getitem__ to enable iteration and membership tests. This underscores how fundamental the sequence protocol is.[file:18]
Implementing a protocol at runtime with monkey-patching
Because protocols are about behavior, you can often make a class support a protocol just by adding the right methods—even at runtime.
Consider a simple FrenchDeck that already supports sequence behavior:[file:18]
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck:
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()
def __init__(self):
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]
def __len__(self):
return len(self._cards)
def __getitem__(self, position):
return self._cards[position]random.shuffle needs a mutable sequence, i.e. it calls __setitem__ to swap elements.[file:18] We can add this method at runtime:
def set_card(deck, position, card):
deck._cards[position] = card
FrenchDeck.__setitem__ = set_card
from random import shuffle
deck = FrenchDeck()
shuffle(deck)
print(deck[:5])This technique is called monkey-patching: changing a class or module at runtime without touching its source code.[file:18]
Duck typing, accidental similarity, and "goose typing"
Duck typing in Python means:
- Prefer checking behavior ("can it be iterated?", "does it support
.draw()?") over checking types withisinstanceortype(...) is ....[file:18] - If an object supports the operations we need, we accept it, regardless of its class.
However, relying purely on method names can lead to accidental similarity: unrelated types that happen to have the same method names.[file:18]
class Artist:
def draw(self): ...
class Gunslinger:
def draw(self): ...
class Lottery:
def draw(self): ...These all have a draw() method but clearly model very different concepts.[file:18] Duck typing would treat them as compatible if we only check for hasattr(x, "draw"), but the semantics differ.
Alex Martelli suggests complementing duck typing with what he calls "goose typing":[file:18]
- When a class is an Abstract Base Class (i.e. its metaclass is
abc.ABCMeta), it is legitimate to useisinstance(obj, ThatABC). - This combines behavioral checks (provided by the ABC’s methods) with a more explicit declaration of intent.
ABCs also allow virtual subclassing via register, letting you declare that a class implements an ABC’s interface without inheritance, as long as the behavior matches.[file:18]
ABCs as semantic contracts (e.g. collections.abc, numbers)
Abstract Base Classes in collections.abc and numbers embody common concepts like "sized", "sequence", "mapping", and "number".[file:18]
Example: collections.abc.Sized represents things that define __len__.[file:18]
class Struggle:
def __len__(self):
return 23
from collections import abc
isinstance(Struggle(), abc.Sized) # TrueHere, Struggle is recognized as a Sized instance without explicit registration, because abc.Sized uses a special class method __subclasshook__ that checks for the presence of __len__ in the class’s MRO.[file:18]
Similarly, the numbers module defines a "numeric tower":
Number>Complex>Real>Rational>Integral.[file:18]
You can write robust numeric checks such as:
import numbers
if isinstance(x, numbers.Integral):
# accept int, bool, or other integer-like types that registered
...External types can register as virtual subclasses of these ABCs when they satisfy the corresponding contracts.[file:18]
When to use ABCs vs pure duck typing
ABCs are useful when:
- You need clear, shared semantic contracts across a codebase or framework.
- You want tools like
isinstance(obj, collections.abc.Sequence)to express intent. - You want default method implementations on the ABC itself (e.g.
MutableSequenceoffers many methods built on a few primitives).[file:18]
But the notes strongly warn against overusing ABCs and metaclasses:
- Avoid defining your own ABCs in everyday application code unless you have a compelling, shared contract to model.[file:18]
- Don’t replace obvious polymorphism with long
if/elifchains ofisinstancechecks—that’s usually a design smell.[file:18] - Prefer designing classes so that normal method dispatch chooses the right behavior automatically.
Defining a subclass of an ABC
You can define a class that explicitly subclasses an ABC, like collections.MutableSequence, which represents a mutable sequence interface.[file:18]
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck2(collections.MutableSequence):
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()
def __init__(self):
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]
def __len__(self):
return len(self._cards)
def __getitem__(self, position):
return self._cards[position]
def __setitem__(self, position, value):
self._cards[position] = value
def __delitem__(self, position):
del self._cards[position]
def insert(self, position, value):
self._cards.insert(position, value)Because FrenchDeck2 subclasses MutableSequence, Python expects it to implement all of that ABC’s abstract methods (__len__, __getitem__, __setitem__, __delitem__, insert).[file:18]
Note:[file:18]
- ABCs are not fully validated when the class is defined.
- The check for missing abstract methods happens at instantiation time; trying to create an instance of a class that still has abstract methods will raise
TypeError.
Defining and using your own ABC
You can define your own ABCs using the abc module:
import abc
class Tombola(abc.ABC):
@abc.abstractmethod
def load(self, iterable):
"""Add elements from an iterable."""
@abc.abstractmethod
def pick(self):
"""Remove and return a random element.
Should raise LookupError when empty.
"""
def loaded(self):
"""Return True if there is at least one item, else False."""
return bool(self.inspect())
def inspect(self):
"""Return a sorted tuple with current items."""
items = []
while True:
try:
items.append(self.pick())
except LookupError:
break
self.load(items)
return tuple(sorted(items))Notes:[file:18]
Tombolainherits fromabc.ABC, marking it as an abstract base class.loadandpickare abstract methods and must be implemented by concrete subclasses.loadedandinspectare regular concrete methods implemented in terms ofloadandpick, and are automatically inherited.
This is a classic ABC pattern: define a minimal set of primitives as abstract, and build richer behavior on top of them.
Virtual subclasses via registration
A powerful feature of ABCs is virtual subclassing using register.
Example: declaring a list subclass as a virtual Tombola without traditional inheritance:[file:18]
from random import randrange
@Tombola.register
class TomboList(list):
def pick(self):
if self:
position = randrange(len(self))
return self.pop(position)
else:
raise LookupError('pop from empty TomboList')
load = list.extend
def loaded(self):
return bool(self)
def inspect(self):
return tuple(sorted(self))
# Tombola.register(TomboList) # equivalent explicit formKey points:[file:18]
@Tombola.registeraddsTomboListas a virtual subclass ofTombola.TomboListdoes not inherit fromTombola, and Python does not automatically check that it implements the required methods—this is a promise you make as the author.[file:18]- Tools like
isinstance(obj, Tombola)will now returnTrueforTomboListinstances.
ABCs keep track of:
- Real subclasses via
__subclasses__()(does not include virtual subclasses).[file:18] - Registered virtual subclasses via an internal registry (
_abc_registry).[file:18]
Automatic recognition via __subclasshook__
Some standard ABCs can recognize suitable classes automatically via __subclasshook__.
Example revisited:[file:18]
class Struggle:
def __len__(self):
return 23
from collections import abc
isinstance(Struggle(), abc.Sized) # True
issubclass(Struggle, abc.Sized) # Trueabc.Sized defines __subclasshook__ roughly like this:[file:18]
class Sized(metaclass=ABCMeta):
@abstractmethod
def __len__(self):
return 0
@classmethod
def __subclasshook__(cls, C):
if cls is Sized:
if any("__len__" in B.__dict__ for B in C.__mro__):
return True
return NotImplementedSo any class that defines __len__ in its MRO is treated as a Sized subclass, even without inheritance or registration.[file:18]
For your own ABCs, implementing __subclasshook__ is rarely worth the complexity; it’s easy to misclassify types based solely on method presence. Prefer explicit inheritance or registration for your own contracts.[file:18]
Practical advice from the notes
If your type clearly matches an existing ABC concept (e.g. sequence, mapping, number), either:
- subclass the appropriate ABC, or
- register your class as its virtual subclass.[file:18]
When you must check argument types (e.g. "is this a sequence?"), prefer ABCs:
pythonimport collections.abc if isinstance(the_arg, collections.abc.Sequence): ...Don’t go overboard defining your own ABCs and metaclasses in application code; they can overcomplicate designs.[file:18]
Use polymorphism instead of long chains of
isinstancetests whenever possible.[file:18]
Overall idea: start with duck typing and simple protocols; reach for ABCs when you need shared, explicit contracts and good isinstance semantics, but avoid turning every concept into a custom hierarchy.[file:18]