Skip to main content

Container: len, getitem, contains

In Lesson 2, James made his notes comparable with __eq__. Now he tries to use the NoteCollection from Chapter 59 the way he would use a list.

He types:

collection = NoteCollection("My Notes")
collection.add(Note("Meeting", "Q3 roadmap", 4))
collection.add(Note("Design", "Architecture plan", 12))
collection.add(Note("Standup", "Daily sync", 2))

print(len(collection))

Output:

TypeError: object of type 'NoteCollection' has no len()

"I have three notes in there," James says. "Why can Python not count them?"

"Because len() calls __len__(). You never wrote __len__(). Python does not know how to count your collection."

He tries indexing:

print(collection[0])

Output:

TypeError: 'NoteCollection' object is not subscriptable

And membership testing:

print(Note("Meeting", "Q3 roadmap", 4) in collection)

Output:

TypeError: argument of type 'NoteCollection' is not iterable

Three TypeErrors. Three special methods needed.

TDG Still Applies

Same cycle, same tools. Each special method is a separate specification: "what should len(collection) return?" Write the test first, then implement the method.


If you're new to programming

When you use len([1, 2, 3]), Python calls the list's __len__ method behind the scenes. When you use my_list[0], Python calls __getitem__. When you use 5 in my_list, Python calls __contains__. These are built into list because someone wrote those special methods. Your custom classes need the same methods to work with the same operations.

If you've coded before

The container protocol in Python is informal: there is no interface to implement. Just define __len__, __getitem__, and __contains__, and the corresponding built-in operations work. __getitem__ that handles integers also enables iteration as a fallback (Python calls __getitem__(0), __getitem__(1), etc.), but explicit __iter__ is preferred (Lesson 4). __contains__ without __getitem__ or __iter__ means only explicit membership testing works.


len: How Many Items?

The simplest container method. len() calls __len__() and expects an integer:

class NoteCollection:
def __init__(self, name: str) -> None:
self.name = name
self._notes: list[Note] = []

def add(self, note: Note) -> None:
self._notes.append(note)

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

collection = NoteCollection("My Notes")
print(len(collection)) # 0

collection.add(Note("Meeting", "Q3 roadmap", 4))
collection.add(Note("Design", "Architecture plan", 12))
print(len(collection)) # 2

Output:

0
2

__len__ delegates to self._notes, which is a regular list. The method is one line. But without it, len(collection) raises TypeError.

James's analogy: inventory count. When the floor manager asks "how many items in this section?", the section must be able to count itself. A pile of boxes on the floor cannot answer that question. A numbered inventory rack can. __len__ turns the pile into the rack.


getitem: Retrieve by Position

__getitem__ lets you access items by index with []:

class NoteCollection:
def __init__(self, name: str) -> None:
self.name = name
self._notes: list[Note] = []

def add(self, note: Note) -> None:
self._notes.append(note)

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

def __getitem__(self, index: int) -> Note:
return self._notes[index]

collection = NoteCollection("My Notes")
collection.add(Note("Meeting", "Q3 roadmap", 4))
collection.add(Note("Design", "Architecture plan", 12))
collection.add(Note("Standup", "Daily sync", 2))

print(collection[0])
print(collection[-1])
print(collection[1:3])

Output:

Note(title='Meeting', word_count=4)
Note(title='Standup', word_count=2)
[Note(title='Design', word_count=12), Note(title='Standup', word_count=2)]

Three things to notice:

  1. collection[0] returns the first note. The method delegates to self._notes[index], which handles both positive and negative indices.

  2. collection[-1] returns the last note. Negative indexing works for free because lists already support it.

  3. collection[1:3] returns a slice. This also works for free because self._notes[index] handles slices. When you pass a slice like 1:3, Python passes a slice(1, 3) object as the index argument.

The type annotation says int, but Python actually passes slice objects for slicing. For a production class, you would annotate index: int | slice and handle both cases. For now, the delegation to self._notes handles it transparently.


contains: Is This Item Present?

__contains__ powers the in operator:

class NoteCollection:
def __init__(self, name: str) -> None:
self.name = name
self._notes: list[Note] = []

def add(self, note: Note) -> None:
self._notes.append(note)

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

def __getitem__(self, index: int) -> Note:
return self._notes[index]

def __contains__(self, item: object) -> bool:
return item in self._notes

meeting = Note("Meeting", "Q3 roadmap", 4)
design = Note("Design", "Architecture plan", 12)
missing = Note("Missing", "Not in collection", 0)

collection = NoteCollection("My Notes")
collection.add(meeting)
collection.add(design)

print(f"meeting in collection: {meeting in collection}")
print(f"missing in collection: {missing in collection}")

Output:

meeting in collection: True
missing in collection: False

__contains__ delegates to item in self._notes, which uses the __eq__ method you wrote in Lesson 2 to compare each note. This is why special methods build on each other: __contains__ uses __eq__, which you implemented to compare title and body.

What would happen if you had not implemented __eq__? The in operator would compare by identity (is), and Note("Meeting", "Q3 roadmap", 4) in collection would return False even though a note with the same data exists in the collection. Each special method extends the others.


All Three Together

Here is the complete NoteCollection with all three container methods:

class NoteCollection:
def __init__(self, name: str) -> None:
self.name = name
self._notes: list[Note] = []

def __repr__(self) -> str:
return f"NoteCollection(name={self.name!r}, count={len(self)})"

def add(self, note: Note) -> None:
self._notes.append(note)

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

def __getitem__(self, index: int) -> Note:
return self._notes[index]

def __contains__(self, item: object) -> bool:
return item in self._notes

Save this as container_demo.py and run:

collection = NoteCollection("My Notes")
collection.add(Note("Meeting", "Q3 roadmap", 4))
collection.add(Note("Design", "Architecture plan", 12))
collection.add(Note("Standup", "Daily sync", 2))

print(f"Count: {len(collection)}")
print(f"First: {collection[0]}")
print(f"Last: {collection[-1]}")
print(f"Has Meeting: {Note('Meeting', 'Q3 roadmap', 4) in collection}")

Output:

Count: 3
First: Note(title='Meeting', word_count=4)
Last: Note(title='Standup', word_count=2)
Has Meeting: True

Three lines of code (one per method) and your collection now works with three Python built-in operations.


PRIMM-AI+ Practice: Container Protocol

Predict [AI-FREE]

Press Shift+Tab to enter Plan Mode before predicting.

Given this class:

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

def add(self, song: str) -> None:
self._songs.append(song)

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

playlist = Playlist("Road Trip")
playlist.add("Bohemian Rhapsody")
playlist.add("Hotel California")

Predict the result of each operation. Write your prediction and a confidence score from 1 to 5.

  1. len(playlist)
  2. playlist[0]
  3. "Bohemian Rhapsody" in playlist
Check your predictions
  1. 2. __len__ returns len(self._songs) which is 2.
  2. TypeError. __getitem__ is not implemented, so indexing fails.
  3. TypeError. __contains__ is not implemented, and without __iter__ or __getitem__, the in operator fails with "argument of type 'Playlist' is not iterable."

Run

Press Shift+Tab to exit Plan Mode.

Create container_practice.py with the Playlist class above. Run all three operations and observe the errors. Then add __getitem__ and __contains__ to fix them. Run again and verify.

Investigate

In Claude Code, type:

/investigate @container_practice.py

Ask: "If I implement __getitem__ but NOT __contains__, does the in operator still work? Why or why not?"


Try With AI

Opening Claude Code

If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.

Prompt 1: Container Protocol for SmartNotes

Here is my NoteCollection class. It has an add() method
and a _notes list. I want len(collection), collection[0],
and note in collection to work.

class NoteCollection:
def __init__(self, name: str) -> None:
self.name = name
self._notes: list[Note] = []

def add(self, note: Note) -> None:
self._notes.append(note)

Write __len__, __getitem__, and __contains__. For each
method, write a test that verifies the method works.

Read the AI's implementation and tests. Verify that __contains__ uses __eq__ for comparison (not identity).

What you're learning: You are seeing how special methods compose: __contains__ relies on __eq__ from Lesson 2. The AI generates both implementation and tests, but you verify the composition is correct.

Prompt 2: The Fallback Chain

In Python, if I implement __getitem__ but NOT __contains__
and NOT __iter__, does the "in" operator work? If so, how
does Python handle it? Show me an example.

What you're learning: Python has a fallback chain for in: it tries __contains__ first, then __iter__, then __getitem__ (calling it with 0, 1, 2, ... until IndexError). Understanding this chain reveals why implementing explicit methods is better than relying on fallbacks.

Prompt 3: Container Methods for Your Domain

I work in [describe your professional domain: logistics,
healthcare, education, finance, etc.]. Design a collection
class for my domain with __len__, __getitem__, and
__contains__. Show me a realistic use case where each
method would be useful.

What you're learning: You are transferring the container protocol from NoteCollection to your own domain. The AI generates a domain-specific collection, and you evaluate whether the three methods cover the operations that would be natural in your context.


James steps back and looks at the NoteCollection. len(collection) returns 3. collection[0] returns the first note. Note("Meeting", "Q3 roadmap", 4) in collection returns True.

"This is like the inventory system," he says. "Asking how many items, pulling item number five off the shelf, and checking if a product is in stock. Each operation needs a different mechanism, but the user does not care about the mechanism. They just ask the question."

Emma tilts her head. "That is exactly the idea behind protocols. You implement the mechanism. Python provides the interface. The user never thinks about __len__. They think about len()."

"I just learned something from you," James says. "I was thinking of these as methods I am writing. But they are contracts I am signing. I am telling Python: 'my collection can be counted, indexed, and searched.' Python trusts me to implement it correctly."

Emma smiles. "And I learned something from your analogy. I never thought of __contains__ as an 'in stock' check. That framing makes the purpose clearer than any documentation I have read." She pauses. "But try for note in collection: and see what happens. Indexing works, but looping does not. Not properly, anyway."