Make a class iterable or container-like¶
The question. You have a class that wraps a collection — a playlist of songs, an inventory of items, a page of records — and you want it to behave like one. for x in obj, len(obj), x in obj, and obj[i] should all work without callers having to reach into obj.items.
Each of those comes from a different dunder method — __iter__, __len__, __contains__, __getitem__ — and you can pick the ones you need. The complete container shape is a handful of one-liners, each delegating to the underlying list.
class Playlist:
def __init__(self, songs=()):
self._songs = list(songs)
def add(self, song):
self._songs.append(song)
def __iter__(self):
# for song in playlist — return a fresh iterator each call
return iter(self._songs)
def __len__(self):
# len(playlist) — also gives you truthiness for free
return len(self._songs)
def __contains__(self, song):
# 'Finale' in playlist
return song in self._songs
def __getitem__(self, index):
# playlist[0], playlist[-1], playlist[::2] — slicing is free
return self._songs[index]
def __repr__(self):
return f'Playlist({self._songs!r})'
p = Playlist(['Prelude', 'Interlude', 'Finale'])
p.add('Encore')
print(p)
print('length:', len(p))
print('Finale in p:', 'Finale' in p)
print('first:', p[0])
print('as list:', list(p))
# Shortcut: inherit from collections.abc for the full suite
from collections.abc import Sequence
class Playlist2(Sequence):
def __init__(self, songs=()):
self._songs = list(songs)
# Sequence requires __getitem__ and __len__; everything else is free.
def __getitem__(self, index):
return self._songs[index]
def __len__(self):
return len(self._songs)
p2 = Playlist2(['Prelude', 'Interlude', 'Finale'])
print('index:', p2.index('Interlude'))
print('count:', p2.count('Prelude'))
print('reversed:', list(reversed(p2)))
Why it works¶
Each dunder plugs into a separate Python protocol, which is why they're independent. for x in obj looks for __iter__; len(obj) looks for __len__; x in obj looks for __contains__; obj[i] looks for __getitem__. Python will fall back sensibly when you don't implement one — in will iterate if there's no __contains__, and iteration itself will call __getitem__(0), (1), (2) until IndexError if there's no __iter__ either. You only implement the ones where you want to supply the behaviour (or outperform the default — a set-backed __contains__ is O(1) versus the O(n) iterate-and-compare fallback).
__iter__ returning iter(self._songs) matters: it hands back a fresh iterator each call, so two nested for loops over the same playlist both see every song. Returning self and implementing __next__ makes the object single-use — fine for a generator-like class, wrong for a container.
The __getitem__ line does a lot of work because self._songs[index] handles both integers and slices. If you were implementing indexing without a backing list, you'd need to branch on isinstance(index, slice) and build the sliced view yourself.
Trade-offs¶
Implement what you need, not the full set. A one-shot pipeline wrapper might need __iter__ and nothing else. A read-only view might need __getitem__ and __len__. Resist the temptation to add every dunder just because you can — each one is a contract you now have to maintain.
collections.abc.Sequence or MutableSequence is the shortcut when you want the complete container API. Implement __getitem__ and __len__ (plus __setitem__, __delitem__, insert for MutableSequence) and the ABC gives you index, count, __contains__, __iter__, and __reversed__ for free. The cost is a subtle API surface — Sequence commits you to indexing semantics, which is overkill for, say, a stream-like object.
Hashability and equality aren't in this recipe, but they're worth naming: if your class is going into a set or is a dict key, it also needs __hash__ and __eq__ — and those have their own gotchas (see "Avoid common class mistakes").
Related reading¶
- Iterators and generators — the protocol under
__iter__, and when__next__earns its place. - Avoid common class mistakes — including the mutable-default trap that bites containers especially hard.
- Truthiness rules — what
__len__gives you for free.