Skip to main content

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.

MethodOperatorDescription
__eq__(self, other)==Equal to
__ne__(self, other)!=Not equal to
__lt__(self, other)&lt;Less than
__le__(self, other)&lt;=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 &lt; 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 with len().
  • __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!