Custom Sequence Types in Python Using Dunder Methods
Python implements sequences through the sequence protocol, a set of
standard conventions powered by container special methods, commonly
known as dunder (double underscore) methods. By defining specific
methods like __len__, __getitem__,
__setitem__, and __delitem__ on a class,
developers can create custom objects that behave identically to built-in
types like lists and tuples. This guide explains the core dunder methods
required to build both immutable and mutable custom sequences, how
Python processes indexing and slicing, and how to leverage standard
library abstractions to simplify implementation.
The Sequence Protocol
In Python, duck typing governs how collections behave. Python does not require a class to inherit from a specific interface to act as a sequence; instead, it checks whether the object implements the sequence protocol.
The sequence protocol is split into two primary categories:
- Immutable Sequences: Require
__len__and__getitem__. - Mutable Sequences: Require
__len__,__getitem__,__setitem__, and__delitem__.
Building Immutable Sequences
An immutable sequence allows elements to be counted, retrieved by index, sliced, and iterated over, but does not allow elements to be modified or deleted.
__len__(self)
The __len__ method is invoked by the built-in
len() function. It must return a non-negative integer
representing the number of items in the container.
def __len__(self):
return len(self._data)__getitem__(self, index)
The __getitem__ method handles element retrieval via
bracket notation (obj[index]). When a user accesses an
index, Python passes that value directly to
__getitem__.
To create a robust sequence, __getitem__ must handle
both integer indices and slice objects:
- Integer Indexing: Handles standard zero-based
access and negative indices (e.g.,
-1for the last item). - Slicing: When syntax like
obj[1:4:2]is used, Python passes asliceinstance to__getitem__. The method should check ifindexis an instance ofsliceand return a new instance of the custom sequence containing the sliced elements.
def __getitem__(self, index):
if isinstance(index, slice):
return self.__class__(self._data[index])
return self._data[index]Implicit Iteration and Membership
If a class defines __len__ and __getitem__,
Python provides iteration (for item in obj) and membership
testing (item in obj) automatically:
- Iteration: Python calls
__getitem__starting at index0and increments the index until anIndexErroris raised. - Membership: Python uses linear search via iteration
to evaluate
inoperations unless an explicit__contains__method is defined.
Building Mutable Sequences
To make a custom sequence mutable, you must implement methods that allow assigning and removing elements.
__setitem__(self, index, value)
Invoked when assigning to an indexed position (e.g.,
obj[0] = "new" or obj[1:3] = [1, 2]). It
receives the key/index and the value being assigned.
def __setitem__(self, index, value):
self._data[index] = value__delitem__(self, index)
Invoked when using the del statement on an element
(e.g., del obj[0] or del obj[1:3]).
def __delitem__(self, index):
del self._data[index]Complete Example: A Custom List Wrapper
The following example demonstrates a custom mutable sequence that wraps a standard Python list:
class CustomList:
def __init__(self, iterable=None):
self._items = list(iterable) if iterable is not None else []
def __len__(self):
return len(self._items)
def __getitem__(self, index):
if isinstance(index, slice):
return self.__class__(self._items[index])
return self._items[index]
def __setitem__(self, index, value):
self._items[index] = value
def __delitem__(self, index):
del self._items[index]
def __repr__(self):
return f"{self.__class__.__name__}({self._items})"With only these four methods implemented, CustomList
supports:
- Item access:
cl[0] - Slicing:
cl[1:3] - Assignment:
cl[0] = 10 - Deletion:
del cl[0] - Iteration:
[x for x in cl] - Membership checks:
10 in cl - Length checking:
len(cl) - Reverse iteration:
reversed(cl)
Using
collections.abc for Full Sequence Features
While implementing dunder methods manually satisfies basic
requirements, production code typically inherits from Python's abstract
base classes in the collections.abc module:
Sequence and MutableSequence.
Subclassing collections.abc.Sequence requires defining
only __len__ and __getitem__. In return, the
base class automatically provides concrete implementations for:
__iter__(efficient iteration)__contains____reversed__index()count()
Subclassing collections.abc.MutableSequence requires
__len__, __getitem__,
__setitem__, __delitem__, and
insert(). The base class automatically supplies:
append()extend()pop()remove()reverse()__iadd__
from collections.abc import MutableSequence
class SmartSequence(MutableSequence):
def __init__(self, initial=None):
self._data = list(initial) if initial is not None else []
def __len__(self):
return len(self._data)
def __getitem__(self, index):
return self._data[index]
def __setitem__(self, index, value):
self._data[index] = value
def __delitem__(self, index):
del self._data[index]
def insert(self, index, value):
self._data.insert(index, value)By defining the container dunder methods directly or subclassing abstract base classes, Python seamlessly integrates custom data structures into the broader language ecosystem.