# Building Custom Containers — Data Structures in Python

Source: https://www.skillbyai.com/en/data-structures-python/p-custom

> 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.

```python
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 Sequence
```

## Fitting 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.

**Quiz:** If you subclass collections.abc.Sequence and implement __getitem__ and __len__, what do you get for free?

- [ ] Nothing
- [x] 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.
