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 an IndexError.

  • Slicing: The [] operator on our internal _cards list understands slicing, so our __getitem__ implementation automatically passes that functionality on to our FrenchDeck.

  • Standard Library Integration: Many standard library functions are built to work with sequences. For example, random.choice works by getting the length of the sequence via len() 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