Introduction
In the previous lesson, we explored Python's concept of "truthiness" and discovered that it's controlled by the __bool__ and __len__ methods. This was our first look into the Python data model, a core system that allows your custom objects to integrate seamlessly with the language's syntax and built-in functions.
This lesson dives deep into that model. Our goal is to explain how the data model integrates custom objects with Python via dunder methods, specifically focusing on __repr__, __len__, and __iter__. By the end of this lesson, you'll understand how to make your own classes "speak Python" fluently, so they can be used with print(), len(), and for loops just like built-in types such as lists or dictionaries.
For someone coming from a Java background, this is Python's answer to mechanisms like implementing interfaces (Iterable, Comparable) or overriding methods from Object (toString()). However, Python's approach is based on informal "protocols" rather than rigid contracts, making it both powerful and flexible.
The Python Data Model: Protocols over Interfaces
At its heart, the Python data model is a collection of protocols your objects can adopt to gain specific behaviors. These protocols are defined by implementing special methods with double underscores, often called "dunder" or "magic" methods.
Instead of writing my_object.get_length(), Python developers expect to write len(my_object). Why? Because len() is a consistent, universal interface for any object that has a concept of length. The len() function works by invoking the object's __len__ method behind the scenes.
Let's start with a foundational reading that explains this philosophy.
This document from UC Irvine provides an excellent academic overview of the Python data model. It establishes the core idea that the mechanisms powering built-in types are available for you to use in your own classes.
Read the introduction and the first few paragraphs down to 'So, let's dive in and see what else we can find in the Python data model.' Focus on how Python uses protocols and dunder methods to allow your custom objects to 'plug into' the language.
Now, let's see a practical demonstration of this concept. The following video introduces the idea that common operations are just syntactic sugar for dunder method calls.
Please Master This MAGIC Python Feature... 🪄
This video from Tech With Tim provides a dynamic introduction to dunder methods, showing how operations like + and len() map directly to methods like __add__ and __len__.
Watch from 01:09 to 05:06. Pay attention to how the presenter demonstrates that everything in Python is an object and that operators and built-in functions work by calling dunder methods defined on the object's class.
1. Unambiguous Representations with __repr__
When you're debugging, one of the most common things you do is print an object to inspect its state. By default, printing a custom object gives you a useless memory address, an experience you're likely familiar with from Java's default Object.toString() behavior.
The "Java-in-Python" Way (Default Behavior):
class Card:
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit
# Create a card
c = Card('7', 'Diamonds')
# Print it
print(c)
# Output: <__main__.Card object at 0x10318e4d0>
This output tells you nothing about the card's rank or suit. To fix this, we implement __repr__.
The goal of __repr__ is to provide an unambiguous, developer-focused string representation of the object. The official convention is that the representation should be a valid Python expression that could be used to recreate the object with the same state, if possible.
The Pythonic Way (Implementing __repr__):
class Card:
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit
def __repr__(self):
# f-string provides a clean way to format this
return f"Card(rank='{self.rank}', suit='{self.suit}')"
# Create and print again
c = Card('7', 'Diamonds')
print(c)
# Output: Card(rank='7', suit='Diamonds')
This is far more useful for debugging. You can immediately see the object's state.
This video segment explains the role of __repr__ and its close cousin, __str__. While our focus is on __repr__ (the developer-facing representation), it's good to know that __str__ exists for more user-friendly, readable output. If only __repr__ is defined, it's used for both.
Please Master This MAGIC Python Feature... 🪄
Let's watch a demonstration of implementing __repr__ and __str__. Notice the distinction: __repr__ is for developers, __str__ is for end-users.
Watch from 06:35 to 12:47. The presenter first shows the problem with printing a custom object and then implements __str__ to fix it. He then introduces __repr__ as the method for developer output and debugging. Focus on the purpose and standard format of the __repr__ output.
2. Sized Collections with __len__
As we saw in the last lesson, the __len__ method is what allows the built-in len() function to work on your objects. It also determines truthiness if __bool__ is not defined.
Let's build a Deck of cards. A deck should have a size that changes as we draw cards.
Without __len__:
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class Deck:
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]
deck = Deck()
# This will fail
# len(deck)
# TypeError: object of type 'Deck' has no len()
To make len(deck) work, we simply delegate the call to the underlying list of cards.
With __len__:
# Continuing the class from above...
class Deck:
# ... (init code from before) ...
def __len__(self):
return len(self._cards)
deck = Deck()
print(f"The deck has {len(deck)} cards.")
# Output: The deck has 52 cards.
By adding one simple method, our Deck object now correctly participates in the "sized" object protocol.
The ICS 33 notes you read earlier go into great detail about implementing __len__, including important performance considerations. For our Deck, delegating to len(self._cards) is an O(1) operation because Python lists store their length internally. However, if you were to calculate length by iterating, it would be O(n). Always prefer an O(1) implementation for __len__ if possible.
3. Iterable Objects with __iter__
The final piece of the puzzle for today is making our Deck iterable. Being iterable is arguably one of the most important protocols, as it allows an object to be used in for loops, comprehensions, and with unpacking.
Without __iter__:
deck = Deck()
# This will fail
# for card in deck:
# print(card)
# TypeError: 'Deck' object is not iterable
To make an object iterable, you must implement the __iter__ method. This method is expected to return an iterator. An iterator is an object that produces the next value in a sequence when you call next() on it, and raises a StopIteration exception when it's exhausted.
The simplest way to implement __iter__ is often to delegate to the __iter__ method of a container you already have, like our self._cards list.
With __iter__:
# Continuing the Deck class...
class Deck:
# ... (init and len code from before) ...
def __iter__(self):
return iter(self._cards)
deck = Deck()
# Now we can loop!
for card in deck:
print(card)
# Output:
# Card(rank='2', suit='spades')
# Card(rank='3', suit='spades')
# ... and so on
Just like that, our Deck class is now iterable.
To understand the mechanics of the iteration protocol (__iter__ and __next__) more deeply, this video provides a clear, step-by-step implementation.
Please Master This MAGIC Python Feature... 🪄
This segment explains the iteration protocol from the ground up, showing how for loops call __iter__ to get an iterator, and then repeatedly call __next__ on that iterator.
Watch from 21:27 to 24:09. The Countdown example is a great demonstration of how to manually manage the state of an iterator and raise StopIteration to signal the end.
A quick note on a related protocol: If a class implements __len__ and __getitem__ (for indexed access like obj[i]), Python can actually iterate over it as a fallback, even without __iter__. This demonstrates how deeply these protocols are integrated. However, implementing __iter__ is the explicit and correct way to define iteration behavior.
Conclusion
By implementing just three dunder methods, we've transformed a clunky, opaque class into a fully-fledged Pythonic object that feels native to the language.
Key Takeaways:
- The Data Model is a set of protocols: By implementing dunder methods like
__repr__,__len__, and__iter__, you allow your custom objects to participate in core Python syntax and behaviors. __repr__for Developers: Implement__repr__to return an unambiguous string representation of your object. This is crucial for effective logging and debugging. The goal iseval(repr(obj)) == obj.__len__for Size: Implement__len__to allow the built-inlen()function to work on your object. This is essential for any object that represents a collection.__iter__for Iteration: Implement__iter__to make your object iterable, allowing it to be used inforloops, comprehensions, and with other iteration-based tools. It should return an iterator.
In our next lesson, we will synthesize what we've learned about Pythonic philosophy and the data model into a practical checklist. We will address the final learning outcome of this module: Apply a checklist to refactor Java-in-Python anti-patterns: replace getters/setters, excessive hierarchies, and type-check gates. You'll learn how to spot and systematically eliminate these patterns from your code.