Dunder Methods that Matter
The special methods that make your objects print well, compare, iterate and act like built-ins.
Beyond the arithmetic operators, a handful of dunder methods decide how well your class integrates with the rest of Python. Implement the right ones and your object prints cleanly, works in for loops, and behaves in collections.
The essential four
__repr__— the unambiguous, developer-facing string. If you write only one, write this. It is what the REPL and debuggers show.__str__— the friendly, user-facing string (falls back to__repr__).__eq__— value equality (pair with__hash__).__len__— makeslen(obj)work and gives truthiness for free.
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f'Point(x={self.x}, y={self.y})'
print(Point(1, 2)) # Point(x=1, y=2)
print([Point(0, 0)]) # [Point(x=0, y=0)] <- repr used in containersMaking an object iterable and callable
__iter__ lets your object drive a for loop; __contains__ powers in; __call__ makes an instance callable like a function; __getitem__ enables obj[key].
Example
class Playlist:
def __init__(self, name, tracks):
self.name = name
self._tracks = list(tracks)
def __repr__(self):
return f'Playlist({self.name!r}, {self._tracks!r})'
def __len__(self): # len() and truthiness
return len(self._tracks)
def __getitem__(self, i): # indexing AND iteration
return self._tracks[i]
def __contains__(self, track): # the 'in' operator
return track in self._tracks
def __call__(self, track): # instance acts like a function
self._tracks.append(track)
return self
pl = Playlist('Focus', ['Aria', 'Nocturne'])
pl('Prelude')('Etude') # __call__, chained
print('repr :', repr(pl))
print('len :', len(pl)) # 4
print('first :', pl[0]) # __getitem__
print('in? :', 'Aria' in pl) # __contains__
print('loop :', [t.upper() for t in pl]) # iterable via __getitem__
print('truthy? :', bool(pl)) # True, from __len__
# Output:
# repr : Playlist('Focus', ['Aria', 'Nocturne', 'Prelude', 'Etude'])
# len : 4
# first : Aria
# in? : True
# loop : ['ARIA', 'NOCTURNE', 'PRELUDE', 'ETUDE']
# truthy? : TrueWhen to use it
- A custom container implements __iter__ and __next__ so it works natively in for loops.
- A domain model object implements __hash__ alongside __eq__ so it can be stored in a set.
- A query builder implements __getattr__ to return a new filtered query for any attribute access.
More examples
__iter__ and __next__ for iteration
Implements the iterator protocol with __iter__ and __next__ so the object works in for loops and list().
class CountUp:
def __init__(self, stop):
self.current = 0
self.stop = stop
def __iter__(self): return self
def __next__(self):
if self.current >= self.stop:
raise StopIteration
self.current += 1
return self.current
print(list(CountUp(5))) # [1, 2, 3, 4, 5]__eq__ and __hash__ together
Defines __eq__ and a matching __hash__ so equal Points compare as one element in a set or dict.
class Point:
def __init__(self, x, y): self.x, self.y = x, y
def __eq__(self, other): return (self.x, self.y) == (other.x, other.y)
def __hash__(self): return hash((self.x, self.y))
p1, p2 = Point(1, 2), Point(1, 2)
print(p1 == p2) # True
print({p1, p2}) # {Point} — only one element__getitem__ for subscript access
Enables [] access and 'in' membership testing by implementing __getitem__ and __contains__.
class Config:
def __init__(self, data): self._d = data
def __getitem__(self, key): return self._d[key]
def __contains__(self, key): return key in self._d
cfg = Config({'host': 'localhost', 'port': 5432})
print(cfg['host']) # localhost
print('port' in cfg) # True
Discussion