Magic Methods (Dunder Methods): `__len__`, `__getitem__`, and More
We've already encountered some of Python's special "dunder" (double underscore) methods, like __init__, __str__, and __repr__. These methods are special because we don't call them directly (e.g., my_obj.__str__()). Instead, Python calls them for us in response to specific actions, like print(my_obj).
This is a core part of Python's data model. By implementing these magic methods, you can make your own custom objects behave just like Python's built-in types, integrating them seamlessly with the language's syntax and functions. This article explores some of the most useful magic methods beyond the basics.
📚 Prerequisites
You should be comfortable with creating classes and defining methods. A good understanding of __str__ and __repr__ is also helpful.
🎯 Article Outline: What You'll Master
In this article, you will learn:
- ✅ Container Methods: How to implement
__len__and__getitem__to make your objects behave like lists or dictionaries. - ✅ Comparison Methods: How to implement methods like
__eq__and__lt__to allow your objects to be compared with==,!=,<,>etc. - ✅ The Power of Python's Data Model: Appreciate how these methods allow for elegant, readable, and "Pythonic" code.
🧠 Section 1: Emulating Container Types
Have you ever wondered how len() knows the length of a list, or how my_list[0] gets the first item? It's all done with magic methods. By implementing them in your own classes, you can create objects that act like containers.
__len__(self)
This method is called by the built-in len() function. It should return a non-negative integer representing the "length" of your object.
Example: A Deck of Cards
Let's create a Deck class that holds a list of cards.
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]
def __len__(self):
"""Returns the number of cards remaining in the deck."""
return len(self._cards)
my_deck = Deck()
# Now we can use len() on our custom Deck object!
print(f"A standard deck has {len(my_deck)} cards.")
Output:
A standard deck has 52 cards.
__getitem__(self, position)
This method is called when you use square bracket notation ([]) to access an item. It allows your object to behave like a list (with integer indices) or a dictionary (with keys).
Let's add it to our Deck class.
class Deck:
# ... (previous __init__ and __len__) ...
def __getitem__(self, position):
"""Allows us to get a card at a specific position."""
return self._cards[position]
my_deck = Deck()
# Get the first card
first_card = my_deck[0]
print(f"The first card is: {first_card.rank} of {first_card.suit}")
# Get the last card
last_card = my_deck[-1]
print(f"The last card is: {last_card.rank} of {last_card.suit}")
# It even gives us slicing for free!
top_three = my_deck[:3]
print(f"The top three cards are: {top_three}")
By implementing just __len__ and __getitem__, our Deck object is starting to feel a lot like a real Python list. We can get its length, retrieve items by index, and even slice it.
💻 Section 2: Making Objects Comparable
By default, if you try to compare two custom objects with ==, Python will only return True if they are the exact same object in memory. This is usually not what we want. We want to define our own logic for what makes two objects equal.
This is done with the rich comparison methods.
| Method | Operator | Description |
|---|---|---|
__eq__(self, other) | == | Equal to |
__ne__(self, other) | != | Not equal to |
__lt__(self, other) | < | Less than |
__le__(self, other) | <= | Less than or equal to |
__gt__(self, other) | > | Greater than |
__ge__(self, other) | >= | Greater than or equal to |
Example: A Person Class
Let's create a Person class where two people are considered "equal" if they have the same name and age.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
"""Two people are equal if their name and age are the same."""
# It's good practice to check the type of the other object
if not isinstance(other, Person):
return NotImplemented
return self.name == other.name and self.age == other.age
def __lt__(self, other):
"""A person is 'less than' another if they are younger."""
if not isinstance(other, Person):
return NotImplemented
return self.age < other.age
p1 = Person("Alice", 30)
p2 = Person("Alice", 30) # Same data, but a different object in memory
p3 = Person("Bob", 25)
# Without __eq__, this would be False
print(f"p1 == p2: {p1 == p2}")
# With __lt__, we can now compare them
print(f"p3 < p1: {p3 < p1}")
Output:
p1 == p2: True
p3 < p1: True
A cool feature is that if you implement __eq__ and one of the ordering methods (like __lt__), Python can often infer the others. For more complex cases, you can use the functools.total_ordering decorator to automatically generate all rich comparison methods from just a few.
✨ Conclusion & Key Takeaways
Magic methods are the key to making your classes feel intuitive and "Pythonic." They allow you to define the behavior of your objects so they can interact naturally with the language's built-in features.
Let's summarize the key takeaways:
- Dunder methods power Python's syntax. They are called automatically by functions and operators.
__len__lets your object work withlen().__getitem__lets your object work with square brackets ([]) for indexing and slicing.- Comparison dunder methods (
__eq__,__lt__, etc.) let you define how your objects behave with comparison operators like==and<.
➡️ Next Steps
We've seen how to make our classes behave like built-in types. Next, we'll explore "Data Classes," a modern Python feature that can automatically write a lot of this boilerplate magic method code for you, making simple data-holding classes even easier to create.
Happy coding!