Chapter 1: Protocols: How Python Does Polymorphism
The Magic is a Method
As a senior developer, you know that the best "magic" in software is no magic at all. It's a well-defined, consistent, and predictable system. Python's renowned elegance and the very concept of "Pythonic" code are direct results of such a system: the Python data model and the protocols it defines.
In many languages, polymorphism is achieved through explicit inheritance and formal interfaces. You declare that a class implements ISequence, and the compiler enforces it. Python takes a more dynamic approach. An object is considered a "sequence" if it behaves like one—that is, if it implements the methods that are part of the sequence protocol, such as __len__ and __getitem__. This is often called "duck typing": if it walks like a duck and quacks like a duck, it's a duck. A protocol is simply a formal name for this collection of expected methods.
This chapter is about leveraging these protocols—the APIs of dunder methods—to make your own objects integrate seamlessly with the language's core features.
The Common Approach: A Deck Without Protocols
Before we dive into the Pythonic way, let's consider how a developer coming from a language like Java or C# might first build a deck of cards. The natural inclination is often to create explicit methods for every action.
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeckV1:
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]
# Method to get the number of cards
def count(self):
return len(self._cards)
# Method to fetch a specific card
def get_card(self, position):
return self._cards[position]
This class works, but it's not idiomatic Python. Interacting with it feels clumsy and verbose.
There's no support for the built-in len() function. You can't use the familiar deck[0] syntax. Standard library functions that expect a sequence, like random.choice, won't work with it. This approach forces the user of your class to learn your specific API (.count(), .get_card()) instead of using the universal, built-in syntax of the language.
This is the problem that protocols solve.
A Pythonic Deck of Cards: The Sequence Protocol
Let's refactor the deck to be more Pythonic. By replacing our custom methods with the special dunder methods of the sequence protocol, we are effectively "opting in" to a huge amount of language-level functionality.
The methods __len__ and __getitem__ are the heart of the sequence protocol for immutable containers. Their presence tells the rest of the Python ecosystem how to interact with our object.
Length and Indexing
Because we implemented __len__, we can pass our deck to the len() function. The function doesn't know or care that it's a FrenchDeck; it only cares that the object has a __len__ method.
Similarly, __getitem__ allows the [] syntax for direct access, just like with a list or tuple:
Free Functionality Through a Protocol
Here is where the real power of protocols becomes apparent. By simply defining __getitem__ and __len__, Python understands that our object can be treated as a sequence, which grants us a huge amount of functionality for free.
Iteration: Python knows how to iterate over sequences. When it sees
for card in deck:, it will automatically call__getitem__with integer indexes starting from 0 until it receives anIndexError.Slicing: The
[]operator on our internal_cardslist understands slicing, so our__getitem__implementation automatically passes that functionality on to ourFrenchDeck.Standard Library Integration: Many standard library functions are built to work with sequences. For example,
random.choiceworks by getting the length of the sequence vialen()and then picking an item at a random index via__getitem__.
This is the core takeaway: by implementing the methods of a protocol, you hook your object into the rich semantics of the language. You don't need to inherit from a special base class or register your class anywhere. You just need to provide the expected behavior.
Summary
Writing Pythonic classes is about embracing the language's core protocols. Instead of relying on rigid inheritance hierarchies, Python uses these informal interfaces to achieve polymorphism. When you want your object to behave like a sequence, you implement the sequence protocol. When you want it to behave like a number, you implement the numeric protocol. This approach leads to code that is more flexible, decoupled, and deeply integrated with the language itself.
In the next chapter, we will examine the fundamental methods that govern the entire lifecycle of an object, from its birth to its final representation.
Last updated