Interfaces, Protocols, and ABCs
Source: Fluent Python, 2nd Edition — Chapter 13 by Luciano Ramalho
“Program to an interface, not an implementation.” In Python a type is the operations it supports. Since 3.8 there are four complementary ways to talk about that:
| Quadrant | When | Mechanism |
|---|---|---|
| Duck typing | Runtime, structure | Informal protocols; no class name required |
| Goose typing | Runtime, names | isinstance / issubclass against ABCs (and virtual subclasses) |
| Static typing | Checkers (Mypy) | typing + PEP 484; class names and ABCs in hints |
| Static duck typing | Checkers | typing.Protocol (PEP 544) |
The interpreter handles the top half. The bottom half needs an external checker. Left = structure (methods). Right = named types. This chapter is duck, goose, and static duck — not the full static-hint story of Chapters 8 and 15.
Do not pick one religion. A large codebase uses all four.
Two kinds of protocols
A dynamic protocol is informal: docs and convention. The interpreter’s sequence and iterable protocols live in the data model. You may implement part of it and still be useful.
A static protocol is a typing.Protocol subclass. A checker requires every method listed. Neither kind needs you to inherit the protocol by name — that is the structural part they share.
__getitem__ alone already gives indexing, for, and in (scan from 0). PySequence_Check in C even treats “has __getitem__, not a dict” as sequence-ish. __len__ is still what people expect of a full sequence.
Dynamic vs static protocol
1 / 1A class that only implements __getitem__
Pick an answer to see if you are right.
collections.abc.Sequence documents a full sequence (__getitem__ + __len__; the rest are mixins). list and the interpreter do not depend on that ABC. The ABC is a map and a goose-typing hook.
Programming ducks
Python digs sequences
If __iter__ is missing, iteration tries __getitem__(0), (1), … until IndexError. If __contains__ is missing, in can scan the same way. Those fallbacks are in the interpreter, not in Sequence.__iter__ (the ABC mixins only emulate them).
Follow a protocol and you inherit the ecosystem: random.shuffle wants a mutable sequence (__setitem__). An immutable deck raises TypeError on assignment.
Monkey patches
A monkey patch adds a method at runtime without editing the file. Attach a function as FrenchDeck.__setitem__ and shuffle works. The function may poke _cards — tightly coupled, easy to break on upgrades. It also shows duck typing: shuffle never asks the class name.
Naming the first argument self is convention; a console patch can use deck, position, card. In a module, use self, key, value.
Fail fast
Many bugs only show at runtime even in Java. In Python, reject bad input at the edge. If you will treat data as a list, self._balls = list(iterable) copies any in-memory iterable and fails immediately if it is not iterable. Do not copy if the caller needs in-place mutation (shuffle) or the data is huge — then isinstance(x, abc.MutableSequence) (goose) or len(x) (rejects iterators) or iter(x) (any iterable) is the right first line.
Type hints miss Any, inference, and everything at runtime. Fail fast is the last net.
EAFP without isinstance: treat field_names as a str (replace / split); on AttributeError, assume an iterable of names; then tuple(...) and str.isidentifier. A hint can only say Union[str, Iterable[str]] — not “comma- or space-separated identifiers.”
The second row is still a string with a space — replace/split must produce two names. 1x is not isidentifier().
Goose typing
Alex Martelli: isinstance(obj, cls) is fine when cls is an ABC (ABCMeta). Checking type(foo) is bar kills inheritance. Checking a concrete class kills polymorphism. A chain of if isinstance dispatch is a smell — use methods. Checking an ABC at a plugin boundary (“you must implement this”) is the justified case.
Register a virtual subclass: Sequence.register(FrenchDeck). issubclass becomes true; no methods are inherited. Python does not verify the promise. Mypy still struggles with register (historical issue).
Some ABCs recognize you without register: Struggle with __len__ is abc.Sized via __subclasshook__. That is structural typing on an ABC. Do not write __subclasshook__ for a fat interface like Tombola — a class named Spam with load/pick is not automatically a lottery cage. Sequence has no hook: mappings also have __len__ / __getitem__ / __iter__ but are not sequences.
isinstance(obj, Hashable) true only means __hash__ exists; a tuple of lists still explodes on hash(). isinstance(obj, Iterable) false can still iterate via __getitem__. Trust hash(obj) and iter(obj).
callable(obj) beats isinstance(obj, Callable).
Do not invent ABCs in ordinary apps. Use collections.abc. abc.ABC is the convenient base (metaclass ABCMeta). Stack @classmethod above @abstractmethod; abstractmethod is innermost, next to def.
Python checks abstract methods at instantiation, not import.
collections.abc clusters
Iterable / Container / Sized → Collection (3.6). Then Sequence / Mapping / Set and their mutable children. Mapping views. Iterator ⊂ Iterable. Callable, Hashable. Mixin methods are written against the public interface — override if you can do better (__contains__ + bisect on a sorted sequence).
Subclassing MutableSequence forces __setitem__, __delitem__, and insert, even if you only wanted shuffle. You gain append, pop, extend, +=, and the Sequence mixins.
Without __delitem__ / insert, Deck(...) raises Can't instantiate abstract class.
A small ABC: Tombola
Frameworks need an extension point. Tombola: load(iterable), pick() (raise LookupError when empty). Concrete mixins may use only the interface: a silly inspect by picking everything then loading back; loaded via bool(inspect()). Subclasses should override those. Catch LookupError so IndexError / KeyError still count.
BingoCage shuffles then pop. LottoBlower uses list(iterable) in __init__ (copy, fail fast) and randrange/pop. Extra methods like __call__ are allowed.
Virtual subclass: @Tombola.register on a list subclass, or Tombola.register(TomboList) after the fact (needed for classes you do not own). tuple, str, range, memoryview are registered on Sequence inside _collections_abc when that module is imported.
TomboList.__mro__ is (TomboList, list, object) — Tombola is absent. load = list.extend aliases a method.
Full exists so a complete subclass still instantiates. OnlyPick must stay abstract. After register, issubclass is true and MRO still has no Cage.
Static protocols
Nominal hints cannot describe double(x) that works for float, str, list, Fraction, and a future Vector. A Protocol with __mul__(self, int) -> Self can.
@runtime_checkable makes isinstance work via __subclasshook__ (presence of methods). Hints are ignored at runtime: complex has __float__ only to raise; isinstance(c, SupportsFloat) can be a lie. Prefer c = complex(o) (EAFP) over SupportsComplex tuples when converting.
numbers.Complex is fine for goose typing (NumPy types are registered). The numeric tower is a poor static hint: Number has no methods, Mypy does not treat int as numbers.Number. Use SupportsFloat / SupportsComplex for checkers; distrust them at runtime around complex. Decimal is deliberately not numbers.Real.
Go-style: one method protocols (RandomPicker.pick). Extend by a new protocol that lists Protocol again and repeats @runtime_checkable (not inherited). typeshed names: plain concept (Iterator), SupportsX for methods, HasX for attributes. Verb + -er (Formatter) is the Go habit.
fromcomplex(datum: SupportsComplex) should c = complex(datum) because the protocol does not promise .real / .imag.
Bag must not subclass Picker. If pick is missing, isinstance is false and .pick() would crash — both tests need a real pick.