Skip to content
aviral gupta

// A1.3 · ~30 min · Advanced

Container protocols

After this lesson you can build a class that works with len(), indexing, slicing, in and for loops, and get the rest of a sequence or set API from collections.abc.

Lesson 3 of 6 in A1 The data model

You will be able to

  • Implement __len__, __getitem__, __contains__ and __iter__ so len(), [], in and for work
  • Predict the fallbacks: in and for through __getitem__, and truth through __len__
  • Use collections.abc base classes to get mixin methods from a few abstract ones
  1. Warm-up · Activity 1 of 7

    Warm-up from module I3: a for loop starts by calling iter() on the object. Which method does iter() look for first?

  2. Predict · Activity 2 of 7

    Predict before you read on: Letters defines only __getitem__. What does this print?

    class Letters:
        def __getitem__(self, index):
            return "abc"[index]
    
    
    d = Letters()
    print(list(d), "b" in d)
  3. Practice · Activity 3 of 7

    Fill in the method name so that len(stack) works and an empty Stack counts as false.

    class Stack:
        def __init__(self, items):
            self._items = list(items)
    
        def ____(self):
            return len(self._items)
    def (self):
  4. Practice · Activity 4 of 7

    Queue defines __len__ but no __bool__. What does this print?

    class Queue:
        def __init__(self):
            self.jobs = []
    
        def __len__(self):
            return len(self.jobs)
    
    
    q = Queue()
    print(bool(q), "empty" if not q else "busy")
  5. Practice · Activity 5 of 7

    A class defines only __len__ and __getitem__. Match each operation to what Python calls.

  6. Brain teaser · Activity 6 of 7

    Brain teaser. Countdown is its own iterator. What does this print?

    class Countdown:
        def __init__(self, start):
            self.current = start
    
        def __iter__(self):
            return self
    
        def __next__(self):
            if self.current <= 0:
                raise StopIteration
            self.current -= 1
            return self.current + 1
    
    
    c = Countdown(3)
    print(2 in c, 2 in c)
  7. Apply · Activity 7 of 7

    Mini-task. Write a class Timeline that inherits from collections.abc.Sequence and holds a list of events. Write only __len__ and __getitem__, then show that in, count(), index() and reversed() work too.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

A deck of cards as a Sequence

Deck inherits from collections.abc.Sequence and writes two methods: __len__ and __getitem__. Slicing returns a new Deck, because __getitem__ checks for a slice object. Everything else in the output, in, index(), reversed() and the for loop in the comprehension, comes from the Sequence mixin methods.

main.py

from collections.abc import Sequence


class Deck(Sequence):
    """Cards in order. Two methods written, the rest inherited from Sequence."""

    def __init__(self, cards: list[str]) -> None:
        self._cards = list(cards)

    def __len__(self) -> int:
        return len(self._cards)

    def __getitem__(self, index):
        if isinstance(index, slice):
            return Deck(self._cards[index])
        return self._cards[index]

    def __repr__(self) -> str:
        return f"Deck({self._cards!r})"


deck = Deck(["A", "K", "Q", "J", "10"])
print(len(deck), deck[0], deck[-1])
print("Top three:", deck[:3])
print("Q" in deck, "2" in deck)
print("Position of J:", deck.index("J"))
print("Reversed:", list(reversed(deck)))
print("Face cards:", [card for card in deck if card in ("K", "Q", "J")])

Run it with

python main.py

Output

5 A 10
Top three: Deck(['A', 'K', 'Q'])
True False
Position of J: 3
Reversed: ['10', 'J', 'Q', 'K', 'A']
Face cards: ['K', 'Q', 'J']
  • deck[-1] works because the list inside handles negative indexes.
  • deck[:3] passes slice(None, 3, None) to __getitem__, which wraps the result in a new Deck.
  • in, index() and reversed() are Sequence mixins built on __len__ and __getitem__.
  • The comprehension loops with the inherited __iter__, which stops at the first IndexError.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

A playlist container

Give Playlist in main.py four methods: __len__ (the number of songs), __getitem__ (the title at a position; IndexError past the end), __contains__ (a title check that ignores case, False for non-strings) and __iter__ (the titles in order).

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    Delegate to the list: len(self._songs), self._songs[index] and iter(self._songs). The list already raises IndexError.

  2. Hint 2

    For __contains__, return False unless isinstance(title, str), then compare lower-case versions.

  3. Hint 3

    return title.lower() in [song.lower() for song in self._songs]

Show a solution

One way to solve it. Yours can look different and still pass the checks.

from collections.abc import Iterator


class Playlist:
    """Song titles in play order."""

    def __init__(self, songs: list[str]) -> None:
        self._songs = list(songs)

    def __len__(self) -> int:
        return len(self._songs)

    def __getitem__(self, index: int) -> str:
        return self._songs[index]

    def __contains__(self, title: object) -> bool:
        if not isinstance(title, str):
            return False
        return title.lower() in [song.lower() for song in self._songs]

    def __iter__(self) -> Iterator[str]:
        return iter(self._songs)


if __name__ == "__main__":
    mix = Playlist(["Blue Monday", "Heroes", "Vienna"])
    print(len(mix), mix[0], "heroes" in mix, list(mix))
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

from collections.abc import Iterator


class Playlist:
    """Song titles in play order."""

    def __init__(self, songs: list[str]) -> None:
        self._songs = list(songs)

    # Add __len__, __getitem__ (by position), __contains__ (ignores case)
    # and __iter__ (yields the titles in order).


if __name__ == "__main__":
    mix = Playlist(["Blue Monday", "Heroes", "Vienna"])
    print(len(mix), mix[0], "heroes" in mix, list(mix))

test_main.py

from main import Playlist


def test_len_and_truth():
    """len() counts the songs; an empty playlist is false"""
    got = len(Playlist(["a", "b", "c"])), bool(Playlist([]))
    assert got == (3, False), f"len() and bool() gave {got!r}, expected (3, False)"


def test_index():
    """p[0] and p[-1] give the first and last song"""
    p = Playlist(["Blue Monday", "Heroes", "Vienna"])
    got = p[0], p[-1]
    assert got == ("Blue Monday", "Vienna"), f"p[0], p[-1] gave {got!r}"


def test_index_out_of_range():
    """An index past the end raises IndexError"""
    try:
        Playlist(["a"])[5]
    except IndexError:
        return
    raise AssertionError("Playlist(['a'])[5] did not raise IndexError")


def test_contains_ignores_case():
    """in ignores upper and lower case"""
    p = Playlist(["Blue Monday", "Heroes"])
    got = "heroes" in p, "HEROES" in p, "Vienna" in p, 42 in p
    assert got == (True, True, False, False), f"the in tests gave {got!r}, expected (True, True, False, False)"


def test_iteration():
    """A for loop gives the titles in order"""
    got = list(Playlist(["x", "y"]))
    assert got == ["x", "y"], f"list(Playlist(['x', 'y'])) gave {got!r}"

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Exercise 2 of 2

A set from three methods

Make TagSet in main.py a subclass of collections.abc.Set[str] and write its three abstract methods, __contains__, __iter__ and __len__, using self._tags. The Set mixins then give you &, |, -, <= and == for free.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    Change the class line to class TagSet(Set[str]):.

  2. Hint 2

    The mixins build new sets by calling TagSet(iterable), which the existing __init__ already accepts.

  3. Hint 3

    __contains__ returns tag in self._tags, __iter__ returns iter(self._tags), and __len__ returns len(self._tags).

Show a solution

One way to solve it. Yours can look different and still pass the checks.

from collections.abc import Iterable, Iterator, Set


class TagSet(Set[str]):
    """Tags without duplicates, kept in the order first seen."""

    def __init__(self, tags: Iterable[str]) -> None:
        self._tags = list(dict.fromkeys(tags))

    def __repr__(self) -> str:
        return f"TagSet({self._tags!r})"

    def __contains__(self, tag: object) -> bool:
        return tag in self._tags

    def __iter__(self) -> Iterator[str]:
        return iter(self._tags)

    def __len__(self) -> int:
        return len(self._tags)


if __name__ == "__main__":
    python = TagSet(["code", "python", "code", "web"])
    print(python)
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

from collections.abc import Iterable, Iterator, Set


class TagSet:
    """Tags without duplicates, kept in the order first seen."""

    def __init__(self, tags: Iterable[str]) -> None:
        self._tags = list(dict.fromkeys(tags))

    def __repr__(self) -> str:
        return f"TagSet({self._tags!r})"

    # Make TagSet a subclass of Set[str] and write the three abstract
    # methods: __contains__, __iter__ and __len__.


if __name__ == "__main__":
    python = TagSet(["code", "python", "code", "web"])
    print(python)

test_main.py

from collections.abc import Set

from main import TagSet


def test_is_a_set():
    """TagSet is a collections.abc.Set"""
    assert isinstance(TagSet([]), Set), "TagSet does not inherit from collections.abc.Set"


def test_no_duplicates():
    """Duplicates are dropped and the first order is kept"""
    got = list(TagSet(["b", "a", "b"])), len(TagSet(["b", "a", "b"]))
    assert got == (["b", "a"], 2), f"list() and len() gave {got!r}, expected (['b', 'a'], 2)"


def test_operators_from_mixins():
    """&, | and - come from the Set mixins"""
    a, b = TagSet(["x", "y"]), TagSet(["y", "z"])
    got = sorted(a & b), sorted(a | b), sorted(a - b)
    assert got == (["y"], ["x", "y", "z"], ["x"]), f"&, | and - gave {got!r}"


def test_comparisons_from_mixins():
    """<= tests for a subset, == compares contents"""
    got = TagSet(["x"]) <= TagSet(["x", "y"]), TagSet(["a", "b"]) == TagSet(["b", "a"])
    assert got == (True, True), f"<= and == gave {got!r}, expected (True, True)"

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Common mistakes

__len__ returning a negative number

class Stack:
    def __init__(self):
        self.items = []

    def __len__(self):
        return -1


print(len(Stack()))

What Python prints

ValueError: __len__() should return >= 0

Why, and the fix

len() checks the result: it must be an int of 0 or more. Return the real count, usually len() of the collection you wrap. For "unknown size", a class should not define __len__ at all.

Forgetting an abstract method

from collections.abc import Sequence


class Deck(Sequence):
    def __init__(self, cards):
        self.cards = list(cards)

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


deck = Deck(["A", "K", "Q"])

What Python prints

TypeError: Can't instantiate abstract class Deck without an implementation for abstract method '__len__'

Why, and the fix

Sequence has two abstract methods, __getitem__ and __len__, and an instance can only be created once both exist. The message names the missing one. Add def __len__(self): return len(self.cards).

A mapping-style __getitem__ in a for loop

class Scores:
    def __init__(self, by_name):
        self.by_name = by_name

    def __getitem__(self, name):
        return self.by_name[name]


scores = Scores({"ada": 90, "bob": 72})
for score in scores:
    print(score)

What Python prints

KeyError: 0

Why, and the fix

Without __iter__, for falls back to __getitem__(0), (1), … and only an IndexError ends that loop. A mapping raises KeyError, so the loop crashes at 0. Give the class an __iter__, for example return iter(self.by_name).

Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Four methods, four operations

len(x) calls __len__, which must return an int of 0 or more. x[i] calls __getitem__(i); x[1:3] calls it with a slice object, so check isinstance(index, slice) if you support slicing. v in x calls __contains__. for v in x calls iter(x), which calls __iter__ and must return a fresh iterator each time. For a bad index, __getitem__ raises IndexError (sequences) or KeyError (mappings), and TypeError for an index of the wrong type.

The fallbacks

Python fills gaps. Without __contains__, in iterates through __iter__. Without __iter__, both for and in use the old sequence protocol: __getitem__(0), (1), (2) and so on, until IndexError. So a class with only __getitem__ is already iterable, and a KeyError there ends a for loop with a crash. Without __bool__, truth uses __len__: a length of 0 is false. Without __reversed__, reversed() uses __len__ and __getitem__.

collections.abc does the rest

Inherit from an abstract base class, write its abstract methods, and inherit the mixin methods. Sequence needs __getitem__ and __len__, and gives you __contains__, __iter__, __reversed__, index and count. Set needs __contains__, __iter__ and __len__, and gives you <=, ==, &, | and -. Forget an abstract method, and creating an instance raises TypeError. isinstance(x, Sized) also works for classes that just define __len__.

Sources

Last reviewed September 29, 2026