Magic Methods Guide: __len__, __getitem__, __eq__
Magic methods (dunder methods) are special functions Python calls automatically in response to operations on your objects. By implementing __len__ to support len(), __getitem__ for indexing and slicing, and comparison methods like __eq__ and __lt__, you can make custom classes feel like native Python types. This is the essence of Pythonic design—code that's intuitive, readable, and seamlessly integrated with Python's syntax.
Key Takeaways
- Magic methods (e.g.,
__init__,__str__,__len__) are called automatically by Python in response to operations; you never call them directly. __len__(self)lets your object work with the built-inlen()function and return a non-negative integer representing length.__getitem__(self, index)enables square bracket indexing and slicing; Python passes the index/slice and you return the item.- Comparison methods (
__eq__,__ne__,__lt__,__le__,__gt__,__ge__) let you define equality and ordering; implement a few and usefunctools.total_orderingfor the rest. - Rich comparison methods return
NotImplementedfor unsupported types, allowing Python to try the reverse operation on the other object.
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.
Making Objects Len-Compatible: __len__
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.
Enabling Indexing: __getitem__
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). When you index with an integer, Python passes that integer; when you slice, Python passes a slice object.
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.
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.
Comparison Method Reference
| 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.
from functools import total_ordering
@total_ordering
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
if not isinstance(other, Person):
return NotImplemented
return self.name == other.name and self.age == other.age
def __lt__(self, other):
if not isinstance(other, Person):
return NotImplemented
return self.age < other.age
# __le__, __gt__, __ge__ are now automatically defined!
Frequently Asked Questions
When should I return NotImplemented instead of False in comparison methods?
Return NotImplemented when you cannot meaningfully compare your object with the other object (e.g., comparing a Person to a string). This tells Python to try the reverse operation on the other object. Return False only when you are certain the objects are not equal; returning NotImplemented gives Python more flexibility.
What's the difference between __str__ and __repr__?
__str__() is for end-users and should be readable and human-friendly (called by str() and print()). __repr__() is for developers and should be unambiguous, ideally showing the code needed to recreate the object (called by repr() and the interactive interpreter). If you only implement one, implement __repr__.
Can I use magic methods with custom operators like + or *?
Yes! Magic methods like __add__, __sub__, __mul__, and __truediv__ let you define behavior for arithmetic operators. For example, __add__(self, other) is called when you use self + other.
Do magic methods slow down my code?
No. Magic methods are compiled into bytecode and are just as efficient as regular method calls. Python's implementation is highly optimized.
How do I make my custom object sortable (work with sorted())?
Implement __lt__ (less than) and optionally use @functools.total_ordering to fill in the other comparison methods. Then sorted(list_of_objects) will work correctly, sorting by the logic you define in __lt__.
Conclusion
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. By implementing __len__ for container size, __getitem__ for indexing, and comparison methods for equality and ordering, you unlock elegant, readable code that other Pythonistas will admire.
Challenge Yourself: Create a custom SortedList class that maintains a list of items in sorted order. Implement __len__, __getitem__, and __lt__ so that instances can be indexed, sliced, compared for length, and sorted with other SortedList objects. Add an add() method that inserts items while maintaining sort order.