Skip to main content

Iteration: iter and next

In Lesson 3, James made NoteCollection support len(), indexing, and in. Now he tries the most natural operation: a for loop.

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))

for note in collection:
print(note)

He runs it. It actually works:

Output:

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

"Wait," James says. "I did not implement __iter__. Why does this work?"

"Because you implemented __getitem__," Emma says. "Python has a fallback: if __iter__ is missing but __getitem__ exists, Python calls __getitem__(0), __getitem__(1), __getitem__(2), and so on until it gets an IndexError. It works, but it is a hack. You should implement __iter__ explicitly."

"Why? The fallback works."

"Try list(reversed(collection)). Or try any(n.word_count > 10 for n in collection). The fallback handles simple for loops, but explicit __iter__ handles everything correctly and communicates your intent."

TDG Still Applies

Same cycle, same tools. Specify what for note in collection: should produce, write a test that iterates and collects results, then implement __iter__.


If you're new to programming

A for loop needs an object that can produce items one at a time. Python calls __iter__() to ask the object for an iterator (something that produces items). Then it calls __next__() on that iterator to get each item. When there are no more items, the iterator signals "I am done" by raising StopIteration. You do not see any of this; the for loop handles it automatically.

If you've coded before

The iteration protocol has two parts: __iter__ returns an iterator object, and __next__ returns the next value or raises StopIteration. For collections wrapping a list, the simplest approach is a generator-based __iter__ using yield from self._items. This avoids writing a separate iterator class. Python's for, comprehensions, map(), filter(), any(), all(), sum(), and unpacking all consume iterators.


The Iteration Protocol

Python's for loop is syntactic sugar for a two-step protocol:

# This for loop:
for note in collection:
print(note)

# Is equivalent to:
iterator = iter(collection) # Step 1: calls collection.__iter__()
while True:
try:
note = next(iterator) # Step 2: calls iterator.__next__()
print(note)
except StopIteration: # Step 3: iteration complete
break

The for loop hides three operations: calling iter() to get an iterator, calling next() to get each item, and catching StopIteration to stop. You never write this code directly, but understanding the protocol explains why __iter__ is needed.

James's analogy makes this concrete: a conveyor belt in the warehouse. __iter__ starts the belt. __next__ takes the next item off the belt. StopIteration means the belt is empty. The worker (the for loop) keeps taking items until the belt signals "done."


Generator-Based iter

The simplest way to implement __iter__ is with yield:

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

def __iter__(self):
yield from self._notes

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))

for note in collection:
print(note)

Output:

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

yield from self._notes produces each note one at a time. Python handles StopIteration automatically. You do not write a separate iterator class or manage any state.

The yield keyword turns __iter__ into a generator function. Each time the for loop asks for the next item, the function resumes from where it left off and yields the next note. When it runs out of notes, the generator raises StopIteration automatically.


What Iteration Unlocks

Once __iter__ is implemented, your collection works with every Python feature that consumes iterators:

# List comprehension
titles = [note.title for note in collection]
print(titles)

# Generator expression with any()
has_long = any(note.word_count > 10 for note in collection)
print(f"Has long note: {has_long}")

# Generator expression with all()
all_short = all(note.word_count < 20 for note in collection)
print(f"All short: {all_short}")

# Unpacking
first, second, third = collection
print(f"First: {first}")

# sorted() with key
by_length = sorted(collection, key=lambda n: n.word_count)
print(f"Shortest: {by_length[0]}")

Output:

['Meeting', 'Design', 'Standup']
Has long note: True
All short: True
First: Note(title='Meeting', word_count=4)
Shortest: Note(title='Standup', word_count=2)

One method (__iter__) enables list comprehensions, any(), all(), unpacking, sorted(), min(), max(), sum(), enumerate(), and zip(). This is the power of protocols: implement the contract once, and the entire language works with your class.


Manual Protocol Walkthrough

To understand what Python does behind the scenes, manually step through the protocol:

# Create the iterator
it = iter(collection)
print(type(it))

# Call next() to get each item
print(next(it))
print(next(it))
print(next(it))

# One more call after exhaustion
try:
print(next(it))
except StopIteration:
print("No more items")

Output:

<class 'generator'>
Note(title='Meeting', word_count=4)
Note(title='Design', word_count=12)
Note(title='Standup', word_count=2)
No more items

iter(collection) calls __iter__(), which returns a generator (because of yield). Each next() call resumes the generator and yields the next note. After the last note, next() raises StopIteration.


PRIMM-AI+ Practice: Iteration

Predict [AI-FREE]

Press Shift+Tab to enter Plan Mode before predicting.

Given this class:

class NumberBag:
def __init__(self) -> None:
self._numbers: list[int] = []

def add(self, n: int) -> None:
self._numbers.append(n)

def __iter__(self):
yield from self._numbers

bag = NumberBag()
bag.add(10)
bag.add(20)
bag.add(30)

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

  1. list(bag)
  2. sum(bag)
  3. max(bag)
  4. [x * 2 for x in bag]
Check your predictions
  1. [10, 20, 30]. list() consumes the iterator and builds a list.
  2. 60. sum() consumes the iterator and adds all values.
  3. 30. max() consumes the iterator and finds the largest.
  4. [20, 40, 60]. The list comprehension iterates and doubles each value.

Run

Press Shift+Tab to exit Plan Mode.

Create iteration_practice.py with the NumberBag class. Add all four operations as print statements and run:

uv run python iteration_practice.py

Compare each output to your prediction.

Investigate

In Claude Code, type:

/investigate @iteration_practice.py

Ask: "What happens if I call list(bag) twice in a row? Do I get the same result both times, or does the second call return an empty list? Why?"


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: Implement iter for SmartNotes

Here is my NoteCollection class. I need to implement
__iter__ so that for loops work. I want the simplest
possible implementation.

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 __iter__ using yield and write three tests:
one for a basic for loop, one for a list comprehension,
and one for any().

Read the AI's implementation. Verify that it uses yield from self._notes (the simplest approach) rather than a manual iterator class.

What you're learning: You are seeing the simplest correct implementation. The AI might suggest alternatives (returning iter(self._notes), writing a separate iterator class). You evaluate which approach matches the principle: simple delegation for simple collections.

Prompt 2: Iterator vs Iterable

What is the difference between an iterable and an
iterator in Python? My NoteCollection has __iter__ that
uses yield. Is NoteCollection an iterable, an iterator,
or both? Can I call next() directly on the collection?

What you're learning: The distinction between iterable (has __iter__) and iterator (has __iter__ and __next__). Your NoteCollection is iterable but not an iterator. Calling iter(collection) returns a new iterator (the generator) each time, so you can loop over the collection multiple times.

Prompt 3: Iteration in Your Domain

I work in [describe your professional domain: logistics,
healthcare, education, finance, etc.]. Design a collection
class for my domain that supports for loops. Show me
three practical operations (comprehension, any/all, sorted)
that would be useful in my domain.

What you're learning: You are transferring the iteration protocol from NoteCollection to your own domain. The AI generates domain-specific examples of iteration consumers, and you evaluate whether the operations match real workflows in your field.


James watches the for loop run. "One method. Five lines. And now my collection works with everything: for loops, comprehensions, sorting, any, all."

"That is the protocol contract," Emma says. "Implement __iter__, and Python trusts your collection to participate in every iteration-based operation."

"I still forget to implement __iter__ on new collection classes," Emma admits. "I will have __len__ and __getitem__ and __contains__, and then someone writes a list comprehension against my collection and it works through the __getitem__ fallback. I do not even notice it is using the fallback until someone runs reversed() and it breaks."

"So the fallback masks the problem?"

"Exactly. Implement __iter__ explicitly, even when the fallback would work. Your future self will thank you."

James nods. "Your objects display, compare, contain, and iterate. One more piece: I tried putting a Note in a set and got TypeError: unhashable type. What does that mean?"

"That is hashability. And it connects back to something you learned in Chapter 48 about sets. Next lesson."