Blog

Dunder Methods That Make Your Objects Feel Built-In

len(x) calls x.__len__(). a + b calls a.__add__(b). for i in x calls x.__iter__(). Python’s syntax is a set of questions, and dunder methods are how your object answers them.

Implement the right ones and your class stops feeling bolted on.

A value type, done properly

class Money:
    def __init__(self, amount, currency='BRL'):
        self.amount, self.currency = amount, currency
    def __repr__(self):
        return f"Money({self.amount!r}, {self.currency!r})"
    def __eq__(self, other):
        if not isinstance(other, Money): return NotImplemented
        return (self.amount, self.currency) == (other.amount, other.currency)
    def __hash__(self):
        return hash((self.amount, self.currency))
    def __add__(self, other):
        if not isinstance(other, Money): return NotImplemented
        if other.currency != self.currency:
            raise ValueError(f"cannot add {other.currency} to {self.currency}")
        return Money(self.amount + other.amount, self.currency)
    def __lt__(self, other):
        return self.amount < other.amount

a, b = Money(10), Money(10)
print(a == b, a is b)
print(a + Money(5))
print(sorted([Money(30), Money(10), Money(20)]))
print({Money(10), Money(10), Money(20)})

It prints:

True False
Money(15, 'BRL')
[Money(10, 'BRL'), Money(20, 'BRL'), Money(30, 'BRL')]
{Money(10, 'BRL'), Money(20, 'BRL')}

Four separate wins from four methods. Equality by value not identity. Addition. Sorting with no key function, from __lt__ alone. Set deduplication, from __hash__.

__eq__ without __hash__ breaks your object

Define __eq__ and Python sets __hash__ to None, because two objects that compare equal must hash equal and Python cannot guess how:

class NoHash:
    def __init__(self, v): self.v = v
    def __eq__(self, other): return self.v == other.v

try:
    {NoHash(1)}
except TypeError as err:
    print(type(err).__name__ + ':', err)

It prints:

TypeError: unhashable type: 'NoHash'

The object can no longer go in a set or be a dict key. If your class is a value type, define both, over the same fields, as Money does. If it is genuinely mutable, leaving it unhashable is the correct outcome.

Return NotImplemented, not False

print(a == 'not money')
try:
    a + 5
except TypeError as err:
    print(type(err).__name__ + ':', err)

It prints:

False
unsupported operand type(s) for +: 'Money' and 'int'

Returning NotImplemented tells Python “I cannot handle this, try the other operand”. Python then falls back — to identity comparison for ==, and to a clear TypeError for +. Return False from __eq__ instead and you break the other type’s chance to answer.

Note NotImplemented is a value you return. NotImplementedError is an exception you raise, for abstract methods. Confusing them is common.

Containers get two behaviours free

class Deck:
    def __init__(self, cards): self._cards = list(cards)
    def __len__(self): return len(self._cards)
    def __getitem__(self, i): return self._cards[i]

d = Deck(['A', 'K', 'Q', 'J'])
print(len(d), d[0], d[-1], d[1:3])
print([c for c in d])          # iteration for free from __getitem__
print('K' in d)                # membership for free too

It prints:

4 A J ['K', 'Q']
['A', 'K', 'Q', 'J']
True

Two methods bought indexing, negative indexing, slicing, iteration and in. Iteration and membership come free because Python falls back to __getitem__ when __iter__ and __contains__ are missing. Slicing works because the index is passed straight to the list.

Truthiness

class Basket:
    def __init__(self, items): self.items = items
    def __len__(self): return len(self.items)

print(bool(Basket([])), bool(Basket(['apple'])))

It prints:

False True

if basket: now means “if the basket has anything in it”. Python asks __bool__, and falls back to __len__. Without either, every object is truthy — which is why if my_object: on a class with no __len__ is always True and never the check you meant.

Callable objects

class Multiplier:
    def __init__(self, by): self.by = by
    def __call__(self, x): return x * self.by

triple = Multiplier(3)
print(triple(5), list(map(triple, [1, 2, 3])))

It prints:

15 [3, 6, 9]

An object that behaves like a function but carries state. This is what makes decorators-as-classes and configurable callbacks work.

What to remember

  • __repr__ always; __eq__ and __hash__ together for value types.

  • Return NotImplemented from operators you cannot handle, never False.

  • __len__ and __getitem__ buy iteration, membership and slicing.

  • __bool__ (or __len__) so if obj: means something.

Do not implement dunders you have no use for. Each one is a promise about how your object behaves, and a promise nobody needed is just more to keep true.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.