Back to Blog
Python

Python __getitem__: Implementing Custom Container Access

python **getitem**: Learn how __getitem__ powers obj[key] in Python, how to implement it for custom containers, handle slices, raise proper exceptions, and avoid commo...

pythondunder methodscontainersubscriptioncustom classes
Illustration of a Python object with a square bracket key, representing the __getitem__ method for custom container access.

The __getitem__ method is what makes Python objects support the subscription operator obj[key]. When you write obj[key], Python calls obj.__getitem__(key). This single method is the backbone of lists, dictionaries, and tuples, but it also allows you to define custom container behavior in your own classes. Understanding python **getitem** is essential for building objects that feel native to the language, whether you are implementing a custom sequence, a mapping, or a proxy that intercepts attribute access.

What getitem Does and Why It Matters

At its core, __getitem__ defines how an object responds to square-bracket indexing. The method receives one argument: the key. The key can be any Python object, but for common containers it is typically an integer, a slice, or a string. The method must return the value associated with that key, or raise an appropriate exception if the key is invalid.

Consider a simple class that wraps a list:

class MyList: def __init__(self, items): self._items = items def __getitem__(self, index): return self._items[index]

Now you can use MyList([1, 2, 3])[1] and get 2. This delegation to the internal list is straightforward, but __getitem__ becomes powerful when you need to customize access logic, such as transforming keys, validating them, or computing values on the fly.

The method also affects iteration. When Python iterates over an object without an explicit __iter__ method, it falls back to calling __getitem__ with sequential integers starting from 0 until an IndexError is raised. This means that a class implementing __getitem__ can be iterated even without __iter__, although providing both is usually better for performance and clarity.

Implementing getitem for a Custom Container

When designing a custom container, you need to decide what types of keys your object accepts. A mapping-like container might accept strings or other hashable objects, while a sequence-like container typically accepts integers and slices. The implementation should mirror the behavior of built-in types as closely as possible.

Here is an example of a custom mapping that stores key-value pairs and provides a default value for missing keys:

class DefaultDict: def __init__(self, default_value): self._data = {} self._default = default_value def __setitem__(self, key, value): self._data[key] = value def __getitem__(self, key): return self._data.get(key, self._default)

This class allows obj['missing'] to return the default value instead of raising a KeyError. The __setitem__ method is the counterpart that enables assignment with obj[key] = value. Implementing both together gives you a fully functional mutable container.

For a sequence-like container, you often need to support slicing. Python passes a slice object as the key when you write obj[start:stop:step]. Your __getitem__ method must handle this explicitly if you want slicing to work.

Handling Different Key Types: Integers, Slices, and More

The key passed to __getitem__ can be any object, but you should anticipate the common types. A robust implementation checks the type of the key and responds accordingly. For example, a custom sequence might handle both integers and slices:

class EvenNumbers: def __init__(self, limit): self._limit = limit def __getitem__(self, key): if isinstance(key, slice): start, stop, step = key.indices(self._limit) return [self[i] for i in range(start, stop, step)] if isinstance(key, int): if key < 0: key += self._limit if key < 0 or key >= self._limit: raise IndexError("index out of range") return key * 2 raise TypeError("invalid key type")

This class represents even numbers from 0 to 2 * (limit - 1). It supports negative indices by converting them to positive ones, and it handles slices by using slice.indices() to get a concrete range. The slice.indices(length) method is a built-in utility that normalizes the slice parameters for a given sequence length, handling negative steps and out-of-range values correctly.

If your container is meant to be used with non-integer keys, such as strings, you should raise TypeError for unsupported types. This matches the behavior of built-in types: list['a'] raises TypeError, not IndexError.

Raising the Right Exceptions for Missing Keys

Choosing the correct exception is critical for compatibility with Python's idioms. For a mapping, a missing key should raise KeyError. For a sequence, an out-of-range integer index should raise IndexError. For a slice that is out of bounds, Python typically returns an empty sequence rather than raising an error, but you can customize that behavior if needed.

The distinction matters because Python's in operator and iteration rely on these exceptions. For example, for item in obj calls obj[0], obj[1], etc., until IndexError is raised. If your __getitem__ raises KeyError for an integer index, iteration will stop prematurely or behave unexpectedly.

Consider a class that implements a sparse list where missing entries return a default value:

class SparseList: def __init__(self, size, default=0): self._size = size self._default = default self._data = {} def __getitem__(self, index): if isinstance(index, slice): return [self[i] for i in range(*index.indices(self._size))] if index < 0: index += self._size if index < 0 or index >= self._size: raise IndexError("list index out of range") return self._data.get(index, self._default)

Here, an out-of-range index raises IndexError, which is the correct behavior for a sequence. If you were to raise KeyError instead, code that expects IndexError would break, and iteration would not terminate correctly.

Performance and Runtime Behavior of getitem

The performance of __getitem__ directly affects the performance of indexing, iteration, and membership testing. For a custom container, the cost of a lookup depends on the underlying data structure. A dictionary-backed container provides O(1) average lookup, while a list-backed container provides O(1) index access. If your __getitem__ performs computation or validation on every call, that overhead is added to every access.

Avoid doing expensive work inside __getitem__ if the result is likely to be reused. For example, if your container computes a value from a key and that computation is deterministic, consider caching the result. However, be careful with memory usage and invalidation. Caching is only beneficial when the same key is accessed multiple times.

Another runtime consideration is that __getitem__ is called by Python's built-in functions like len() indirectly? Actually, len() requires __len__, not __getitem__. But iter(obj) may call __getitem__ if __iter__ is absent. In that case, iteration performance is tied to the efficiency of __getitem__ for sequential integer keys. If your __getitem__ is slow, iteration will be slow.

If you need fast iteration, implement __iter__ separately. This also allows you to return a generator or a custom iterator that can be more efficient than repeated __getitem__ calls.

Common Mistakes and Edge Cases When Implementing getitem

One common mistake is forgetting to handle negative indices. Built-in sequences support negative indexing, and your custom container should too if it represents a sequence. Use the pattern if index < 0: index += len(self) to normalize the index before accessing the underlying data.

Another mistake is mishandling slices. If you do not explicitly handle slice objects, your container will raise a TypeError when someone tries to slice it. Use isinstance(key, slice) and delegate to a method that constructs the appropriate result. The slice.indices() method is your friend for normalizing the slice against the sequence length.

A subtle edge case is the behavior of __getitem__ when the key is a boolean. In Python, True and False are subclasses of int, so obj[True] is equivalent to obj[1]. If your container is meant to accept only integer keys, this may be surprising. You can explicitly reject booleans by checking isinstance(key, bool) and raising TypeError if needed.

Also, be mindful of the interaction between __getitem__ and the in operator. The in operator for a container without __contains__ iterates over the container and checks equality. If your __getitem__ raises exceptions for certain keys, membership testing may fail unexpectedly. Implementing __contains__ explicitly can give you more control and better performance.

Using getitem for Advanced Patterns

Beyond basic containers, __getitem__ can be used to create proxy objects that intercept attribute access, implement lazy evaluation, or provide a uniform interface to heterogeneous data sources.

For example, a proxy that forwards indexing to a remote service could look like this:

class RemoteProxy: def __init__(self, url): self._url = url def __getitem__(self, key): # In a real implementation, this would make an HTTP request. # The key is serialized and sent to the server. return fetch_from_remote(self._url, key)

Here, __getitem__ hides the network call behind a familiar syntax. The caller can write proxy['user:42'] without knowing about the underlying protocol.

Another advanced pattern is implementing a read-only view of an underlying data structure. By defining __getitem__ but not __setitem__, you prevent modification through the subscription syntax, which can be useful for exposing immutable interfaces.

Finally, __getitem__ can be combined with __len__ and __iter__ to create objects that fully emulate built-in sequences. This is often done when you need to provide a custom data structure that behaves like a list or a dict but with additional constraints or transformations. The key is to keep the implementation consistent: if you support integer indexing, also support negative indices and slices; if you support iteration, ensure that IndexError is raised at the end.

Mastering __getitem__ gives you the ability to design classes that integrate seamlessly with Python's syntax and idioms. Whether you are building a custom collection, a proxy, or a domain-specific abstraction, the method is a fundamental tool for creating expressive and Pythonic APIs.

python **getitem**: Practical Usage and Code Examples | RYUSLOG DEV