Custom Containers#
Python's data model lets you build objects that behave like built-in containers — supporting len, indexing, iteration, membership, and operators — by implementing the right dunder methods. Custom containers wrap internal storage while exposing a clean, Pythonic API.
How to use this page
Read after Data Structures and Dunder or Magic Methods. For traversal mechanics, see Custom Iterators. For ABC contracts, see Abstract Base Classes.
At a glance
| Track | Python Advanced → Data Model and Customization |
| Sections | 12 major topics |
| Outline | Use the right-hand TOC to jump |
Topics: Which methods to implement? · Sequence example: fixed-capacity ring buffer · Mapping example: case-insensitive dict · Set-like container · Slicing protocol · __bool__ vs __len__ · Hashable immutable containers · Delegation pattern (wrapper containers) · … (+4 more)
- Which methods to implement?
- Sequence example: fixed-capacity ring buffer
- Mapping example: case-insensitive dict
- Set-like container
- Slicing protocol
__bool__vs__len__- Hashable immutable containers
- Delegation pattern (wrapper containers)
collections.UserList/UserDict/UserString- Performance:
__slots__ - When to subclass built-ins
- Testing container contracts
Which methods to implement?#
| You want… | Implement |
|---|---|
len(c) |
__len__ |
c[i], c[i:j] |
__getitem__ (and optionally __setitem__, __delitem__) |
for x in c |
__iter__ (or __getitem__ legacy) |
x in c |
__contains__ (or rely on iteration) |
c + d, c * n |
__add__, __mul__, etc. |
bool(c) |
__len__ (non-zero → True) or __bool__ |
| Hashable keys | __hash__ + immutable __eq__ |
Minimal viable container: __len__ + __getitem__ + __iter__ (often via generator).
Sequence example: fixed-capacity ring buffer#
from collections.abc import Sequence
class RingBuffer[T](Sequence):
def __init__(self, capacity: int) -> None:
self._capacity = capacity
self._data: list[T | None] = [None] * capacity
self._size = 0
self._head = 0
def append(self, item: T) -> None:
self._data[(self._head + self._size) % self._capacity] = item
if self._size < self._capacity:
self._size += 1
else:
self._head = (self._head + 1) % self._capacity
def __len__(self) -> int:
return self._size
def __getitem__(self, index: int) -> T:
if not isinstance(index, int):
raise TypeError("index must be int")
if index < 0:
index += self._size
if index < 0 or index >= self._size:
raise IndexError("index out of range")
return self._data[(self._head + index) % self._capacity] # type: ignore[return-value]
def __iter__(self):
for i in range(self._size):
yield self[i]
buf = RingBuffer[int](3)
for x in [1, 2, 3, 4]:
buf.append(x)
list(buf) # [2, 3, 4]
len(buf) # 3
buf[0] # 2
Inheriting collections.abc.Sequence provides mixin implementations for __contains__, __reversed__, index, and count when __getitem__ and __len__ exist.
Mapping example: case-insensitive dict#
from collections.abc import MutableMapping
class CaseInsensitiveDict(MutableMapping):
def __init__(self, data: dict[str, str] | None = None) -> None:
self._store: dict[str, str] = {}
if data:
for k, v in data.items():
self[k] = v
def _key(self, key: str) -> str:
return key.casefold()
def __getitem__(self, key: str) -> str:
return self._store[self._key(key)]
def __setitem__(self, key: str, value: str) -> None:
self._store[self._key(key)] = value
def __delitem__(self, key: str) -> None:
del self._store[self._key(key)]
def __iter__(self):
return iter(self._store)
def __len__(self) -> int:
return len(self._store)
d = CaseInsensitiveDict({"Name": "Ada"})
d["name"] # 'Ada'
"name" in d # True
MutableMapping mixins supply .get, .update, .keys, .values, .items once the core five methods exist.
Set-like container#
from collections.abc import MutableSet
class OrderedUnique(MutableSet):
def __init__(self, items=()) -> None:
self._order: list = []
self._seen: set = set()
def add(self, item) -> None:
if item not in self._seen:
self._seen.add(item)
self._order.append(item)
def discard(self, item) -> None:
if item in self._seen:
self._seen.discard(item)
self._order.remove(item)
def __contains__(self, item) -> bool:
return item in self._seen
def __iter__(self):
return iter(self._order)
def __len__(self) -> int:
return len(self._order)
Slicing protocol#
If __getitem__ receives a slice, return a new container or sequence:
class Tags:
def __init__(self, items: list[str]) -> None:
self._items = items
def __getitem__(self, key):
if isinstance(key, slice):
return Tags(self._items[key])
return self._items[key]
def __len__(self) -> int:
return len(self._items)
Tags(["a", "b", "c"])[1:3] # Tags with ['b', 'c']
__bool__ vs __len__#
class Bag:
def __init__(self) -> None:
self._items: list = []
def __len__(self) -> int:
return len(self._items)
# if __bool__ undefined, __len__ == 0 → falsy
bool(Bag()) # False
Define __bool__ when emptiness isn't the right truth condition (e.g., a config object that's "truthy" even when empty).
Hashable immutable containers#
class FrozenPoint:
__slots__ = ("x", "y")
def __init__(self, x: int, y: int) -> None:
object.__setattr__(self, "x", x)
object.__setattr__(self, "y", y)
def __eq__(self, other: object) -> bool:
if not isinstance(other, FrozenPoint):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)
def __hash__(self) -> int:
return hash((self.x, self.y))
{FrozenPoint(1, 2): "origin-ish"}
Rule: if __eq__ is defined, equal objects must have equal hashes. Mutable containers must not be dict keys.
Details in Classes and OOP Basics.
Delegation pattern (wrapper containers)#
class ListProxy:
def __init__(self, data: list) -> None:
self._data = data
def __len__(self) -> int:
return len(self._data)
def __getitem__(self, index):
return self._data[index]
def __setitem__(self, index, value) -> None:
self._data[index] = value
def append(self, value) -> None:
self._data.append(value)
Expose only the methods you want — hide mutators for read-only views.
collections.UserList / UserDict / UserString#
Subclass wrappers with a .data attribute — faster to prototype than full ABC implementation:
from collections import UserList
class SortedList(UserList):
def append(self, item) -> None:
super().append(item)
self.data.sort()
SortedList([3, 1]).append(2)
# .data == [1, 2, 3]
Performance: __slots__#
class CompactStack:
__slots__ = ("_items",)
def __init__(self) -> None:
self._items: list = []
def push(self, x) -> None:
self._items.append(x)
def pop(self):
return self._items.pop()
__slots__ reduces per-instance memory; you lose arbitrary attribute assignment and some multiple-inheritance flexibility.
When to subclass built-ins#
| Subclass | Risk |
|---|---|
list, dict |
copy, pickle, C-speed paths may surprise |
ABCs (MutableMapping) |
Clear contract, more boilerplate |
| Composition | Safest — wrap, don't inherit |
# prefer wrapping for production APIs
class Inventory:
def __init__(self) -> None:
self._skus: dict[str, int] = {}
Testing container contracts#
from collections.abc import Sequence
assert isinstance(my_container, Sequence)
assert len(my_container) == 3
assert list(my_container) == expected
assert my_container[1] == expected[1]
Use ABC isinstance checks to verify your object quacks correctly.
Interview traps (quick reference)#
| Trap | What goes wrong | Safe approach |
|---|---|---|
| Mutable object as dict key | TypeError or silent corruption |
Immutable or id-based keys only |
__eq__ without __hash__ |
Sets/dicts break | Define both or __hash__ = None |
Negative index in __getitem__ |
Inconsistent with list | Normalize index |
| Slice returns wrong type | Surprising API | Document return type |
Subclass dict in __init__ |
Shared mutable class attr | Compose internal dict |
Mental model checklist#
- Which dunder methods are required for
len, indexing, andforloops? - What does inheriting
MutableMappinggive you for free? - Why must hashable containers be immutable (or treat equality carefully)?
- When is composition better than subclassing
list? - How does
__getitem__support slicing?
What's next#
| Topic | Page |
|---|---|
| Dunder methods | Dunder or Magic Methods |
| Custom iterators | Custom Iterators |
| ABCs | Abstract Base Classes |
| OOP basics | Classes and OOP Basics |
| Data structures | Data Structures |