Lesson 23 / 25
Building Custom Containers
Implement Pythonic containers with dunder methods and collections.abc.
Containers that feel built in
Python's data model lets your classes behave like built-in containers by implementing special (dunder) methods: __len__ (for len()), __iter__ (for loops and unpacking), __contains__ (for in), __getitem__, __setitem__ and __delitem__ (for indexing and slicing), __repr__ (for debugging) and __eq__. The collections.abc module defines abstract base classes such as Sequence, MutableSequence, Mapping, MutableMapping and Set: inherit from one, implement the few required abstract methods, and you get the rest (such as get, keys, items, __contains__ and index) for free, plus correct isinstance checks. For type hints, use generic types (class Ring[T] with Python 3.12 syntax, or Generic[T] in older versions) and protocols from typing (Iterable, Sized) to accept any object with the right shape. Good custom containers validate input, document complexity, keep invariants private (prefixing internals with an underscore) and raise the same exceptions built-ins do (IndexError, KeyError).
A fixed-size ring buffer as a MutableSequence-like container
Dunder methods plus collections.abc for free extras.
from collections.abc import Sequence
from typing import Iterator
class RingBuffer[T](Sequence[T]): # Python 3.12+ generic syntax
"""Keeps the most recent `capacity` items; O(1) append and indexing."""
def __init__(self, capacity: int) -> None:
if capacity <= 0:
raise ValueError("capacity must be positive")
self._data: list[T | None] = [None] * capacity
self._start = 0
self._size = 0
def append(self, item: T) -> None:
end = (self._start + self._size) % len(self._data)
self._data[end] = item
if self._size < len(self._data):
self._size += 1
else:
self._start = (self._start + 1) % len(self._data) # overwrite the oldest
def __len__(self) -> int:
return self._size
def __getitem__(self, index): # Sequence requires __getitem__ and __len__
if isinstance(index, slice):
return [self[i] for i in range(*index.indices(self._size))]
if index < 0:
index += self._size
if not 0 <= index < self._size:
raise IndexError("ring buffer index out of range")
return self._data[(self._start + index) % len(self._data)]
def __repr__(self) -> str:
return f"RingBuffer({list(self)!r}, capacity={len(self._data)})"
temps = RingBuffer[float](3)
for t in [21.5, 22.0, 23.1, 24.4]:
temps.append(t)
print(temps) # RingBuffer([22.0, 23.1, 24.4], capacity=3)
print(temps[-1], temps[:2]) # 24.4 [22.0, 23.1]
print(23.1 in temps, temps.index(24.4)) # True 2: provided free by SequenceFitting a standard socket
Implementing the dunder methods is like fitting a standard plug to your appliance: once it fits, every socket in the house (for loops, len, in, slicing, library functions) works with it.
Quick check: If you subclass collections.abc.Sequence and implement __getitem__ and __len__, what do you get for free?
- Nothing
- Methods such as __contains__, __iter__, index, count and __reversed__
- Automatic sorting
- Thread safety
Answer
Methods such as __contains__, __iter__, index, count and __reversed__ — The ABC provides mixin methods built on the abstract ones.