Warm-up · Activity 1 of 7
// 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.
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
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)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):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")Practice · Activity 5 of 7
A class defines only __len__ and __getitem__. Match each operation to what Python calls.
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)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.pyOutput
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
Hint 1
Delegate to the list: len(self._songs), self._songs[index] and iter(self._songs). The list already raises IndexError.
Hint 2
For __contains__, return False unless isinstance(title, str), then compare lower-case versions.
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.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 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
Hint 1
Change the class line to class TagSet(Set[str]):.
Hint 2
The mixins build new sets by calling TagSet(iterable), which the existing __init__ already accepts.
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.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyCommon 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 >= 0Why, 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: 0Why, 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.