Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

Python Custom Subscription and Container Behavior

Learn how Python classes customize square-bracket subscription, item assignment, item deletion, reverse iteration, and formal sequence, set, and mapping interfaces.

Levels: Advanced ~34 min read

Advanced

Python lets your own classes participate in subscription and reverse-iteration syntax. Instead of forcing callers to use methods such as get_item() or set_item(), a class can define the special methods that Python uses for expressions such as obj[key], obj[key] = value, del obj[key], and reversed(obj). This lesson focuses on those container-facing hooks.

Make square brackets mean something

Define __getitem__() when a class should support subscription. Python passes the value inside the brackets to this method. Your implementation decides what that value means for the object.

python
1
2
3
4
5
6
7
8
9
class Catalog:
    def __init__(self, names):
        self.names = list(names)

    def __getitem__(self, position):
        return self.names[position]

catalog = Catalog(["Python", "HTML", "CSS"])
print(catalog[1])

The expression catalog[1] calls Catalog.__getitem__() with 1 as the subscript.

Treat a class like a sequence

For a sequence-style object, the subscript is normally an integer or a slice. If you delegate to an internal list or tuple, Python’s usual indexing behavior—including negative indexes and slice handling—comes along naturally.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Playlist:
    def __init__(self, songs):
        self.songs = list(songs)

    def __getitem__(self, item):
        return self.songs[item]

playlist = Playlist(["Intro", "Verse", "Bridge", "Outro"])
print(playlist[1:3])
print(playlist[-1])

The same __getitem__() can handle an integer index and a slice because list indexing already understands both.

Understand slices as subscript values

A slice such as obj[1:3] is handled by the same __getitem__() hook. The value supplied to the method is a slice object. A custom sequence can inspect it or delegate it to another sequence.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Window:
    def __init__(self, items):
        self.items = list(items)

    def __getitem__(self, key):
        if isinstance(key, slice):
            return Window(self.items[key])
        return self.items[key]

window = Window([10, 20, 30, 40])
print(window[1].__class__.__name__, window[1])
print(window[1:3].items)

This class returns a new Window for slices and a single value for an integer index.

Support item assignment with __setitem__

Define __setitem__() when callers should be able to assign through subscription. The method receives the subscript and the new value, so the class controls how the update happens.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Scores:
    def __init__(self, values):
        self.values = list(values)

    def __getitem__(self, index):
        return self.values[index]

    def __setitem__(self, index, value):
        self.values[index] = value

scores = Scores([70, 80, 90])
scores[1] = 85
print(scores[1])

The assignment scores[1] = 85 is routed to __setitem__().

Support deletion with __delitem__

Define __delitem__() when callers should be able to remove an item with the del statement. Like the other subscription hooks, it receives the subscript value.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Registry:
    def __init__(self, values):
        self.values = dict(values)

    def __getitem__(self, key):
        return self.values[key]

    def __setitem__(self, key, value):
        self.values[key] = value

    def __delitem__(self, key):
        del self.values[key]

registry = Registry({"draft": 2, "published": 5})
del registry["draft"]
registry["published"] = 6
print(registry["published"])

The expression del registry["draft"] invokes __delitem__() with "draft".

Give reversed() a custom implementation

Define __reversed__() when your object can provide its own reverse iterator. Python uses it for reversed(obj) when the hook exists. This can be useful when the class has a more direct or efficient way to traverse its data backwards.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Archive:
    def __init__(self, items):
        self.items = tuple(items)

    def __getitem__(self, index):
        return self.items[index]

    def __reversed__(self):
        return reversed(self.items)

archive = Archive(["first", "second", "third"])
print(list(reversed(archive)))

The explicit __reversed__() hook supplies the iterator used by reversed(archive).

Choose sequence or mapping behavior

The same subscription syntax can model different kinds of containers. A sequence usually interprets an integer as an index and a slice as a range. A mapping normally interprets the subscript as a key. Your class should choose and document one clear model instead of accepting arbitrary values without meaning.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class CodeBook:
    def __init__(self, entries):
        self.entries = dict(entries)

    def __getitem__(self, key):
        try:
            return self.entries[key]
        except KeyError:
            raise KeyError(f"unknown code: {key}")

book = CodeBook({"py": "Python"})
try:
    print(book["js"])
except KeyError as error:
    print(error)

A mapping-style container uses keys and preserves mapping-style KeyError behavior.

Use the right errors for invalid subscripts

The official data model recommends TypeError when the subscript has an inappropriate type. For sequence-like objects, an inappropriate index value should normally raise IndexError; for mapping-like objects, a missing key should normally raise KeyError. Delegating to list or dict often gives these behaviors automatically.

Combine the hooks into a mutable sequence

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class RecentItems:
    def __init__(self, items):
        self.items = list(items)

    def __getitem__(self, index):
        return self.items[index]

    def __setitem__(self, index, value):
        self.items[index] = value

    def __delitem__(self, index):
        del self.items[index]

    def __reversed__(self):
        return reversed(self.items)

recent = RecentItems(["A", "B", "C"])
recent[1] = "Beta"
print(recent[0:2])
del recent[0]
print(list(reversed(recent)))

A mutable sequence can combine read, write, delete, and reverse-iteration hooks in one coherent interface.

These hooks are independent. Subscription reads, subscription writes, deletion, and reverse iteration each have their own protocol method. A class can support only the operations that make sense for its design.

Do not confuse __getitem__ with __class_getitem__

This lesson covers subscription of an instance, such as playlist[1]. Python also has a separate mechanism for subscription of a class itself, such as list[int]: __class_getitem__(). That mechanism is part of generic class parameterization and is intentionally covered separately so the two protocols stay clear.

Know when custom container behavior helps

Custom subscription is useful when an object naturally behaves like a sequence or mapping and square-bracket syntax makes that interface clearer. If the operation is better understood as an action with named arguments, a normal method is usually easier to read.

Formalize a read-only sequence with Sequence

The earlier examples show how indexed subscription works. When a class should explicitly promise the standard read-only sequence interface, inherit from collections.abc.Sequence. Implement __getitem__() for integer indexes and slices, and __len__() for the number of items. The ABC then supplies common operations such as count(), index(), iteration, membership, and reverse iteration.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from collections.abc import Sequence

class TopicSequence(Sequence):
    def __init__(self, topics):
        self._topics = tuple(topics)

    def __getitem__(self, index):
        return self._topics[index]

    def __len__(self):
        return len(self._topics)

topics = TopicSequence(["syntax", "mapping", "decorators", "mapping"])
print(topics[1])
print(topics.count("mapping"))
print(topics.index("decorators"))
print(list(reversed(topics)))
print("syntax" in topics)

The tuple provides the indexed storage. Implementing the two required methods lets Sequence provide count(), index(), membership, iteration, and reverse iteration.

These inherited operations may call __getitem__() repeatedly. That is fine for storage with fast indexed access, such as a tuple, but a class with expensive indexing may need to override mixin methods for better performance. Inherit from MutableSequence instead only when item replacement, deletion, and insertion are part of the intended interface; it requires __setitem__(), __delitem__(), and insert() in addition to sequence lookup and length.

Quick check

Which pair lists the abstract methods a concrete collections.abc.Sequence subclass must implement?

required_sequence_methods = 
print(required_sequence_methods)

Formalize a read-only mapping with Mapping

The custom subscription examples above show which special methods make bracket syntax work. When a class should present a dictionary-like interface to other code, inherit from collections.abc.Mapping to declare that contract and receive standard read-only mapping mixins.

A Mapping implementation must provide __getitem__(), __iter__(), and __len__(). Iteration yields keys, not key-value pairs. The abstract base class supplies useful operations such as keys(), items(), values(), get(), and membership testing, built on those methods.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from collections.abc import Mapping

class LanguageLabels(Mapping):
    def __init__(self, labels):
        self._labels = dict(labels)

    def __getitem__(self, key):
        return self._labels[key]

    def __iter__(self):
        return iter(self._labels)

    def __len__(self):
        return len(self._labels)

labels = LanguageLabels({"py": "Python", "html": "HTML"})
print(labels["py"])
print(list(labels.keys()))
print(labels.get("css", "unknown"))
print("html" in labels)

Three required methods are enough for the ABC to provide familiar read-only mapping operations.

Choose MutableMapping only when the type also supports meaningful item assignment and deletion. It adds __setitem__() and __delitem__() to the required interface and supplies mutation helpers such as update(), pop(), and setdefault(). Do not inherit a mutable interface merely to obtain convenience methods if the object is intentionally read-only.

Quick check

Which tuple lists the three abstract methods a read-only Mapping subclass must implement?

required_methods = 
print(required_methods)

Formalize set behavior with Set

Some custom containers represent unordered collections of distinct values rather than positions or key-value pairs. To advertise the standard read-only set interface, inherit from collections.abc.Set. This formalizes how your object participates in membership tests and mathematical set operations without re-teaching the built-in set operations from the Tuples and Sets lesson.

A concrete Set subclass must implement __contains__(), __iter__(), and __len__(). The ABC provides comparisons, &, |, -, ^, and isdisjoint() from that contract.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from collections.abc import Set

class PermissionSet(Set):
    def __init__(self, permissions):
        self._permissions = frozenset(permissions)

    def __contains__(self, permission):
        return permission in self._permissions

    def __iter__(self):
        return iter(self._permissions)

    def __len__(self):
        return len(self._permissions)

granted = PermissionSet({"read", "write"})
requested = PermissionSet({"read", "deploy"})
print(sorted(granted & requested))
print(granted <= PermissionSet({"read", "write", "deploy"}))
print(granted.isdisjoint({"deploy"}))

The three primitive methods let the Set mixin implement intersection, subset comparison, and disjointness. Sorting is used only to make the displayed result deterministic; sets themselves do not promise an iteration order.

Set operations that create another result rely on _from_iterable(), whose default implementation calls the class constructor with an iterable. Keep that constructor form or override the classmethod when your custom type needs different construction. Use collections.abc.MutableSet only when mutation is part of the intended contract; it additionally requires add() and discard() and supplies mutating helpers. The Set ABC does not automatically make an object hashable.

Quick check

Which three abstract methods must a concrete collections.abc.Set subclass provide?

required_set_methods = 
print(required_set_methods)

Key takeaways

Key takeaways

  • __getitem__() implements instance subscription such as obj[key].
  • __setitem__() and __delitem__() add subscription assignment and deletion behavior.
  • A sequence-style object usually accepts integer indexes and slice objects; a mapping-style object uses keys.
  • __reversed__() supplies a custom reverse iterator for reversed(obj).
  • Choose the smallest consistent set of container hooks your class actually needs.
  • __class_getitem__() is a separate protocol for class subscription and generic parameterization.
  • Inheriting collections.abc.Sequence formalizes indexed access and length while providing common read-only sequence operations.
Was this page helpful?

Mark this learning resource complete to track your learning progress.