DMarlon Studio

Lesson 7 · 7 of 20

The iterator protocol · Python

How a for loop works inside: iter(), next(), StopIteration, and a custom Python class with __iter__ and __next__ that pages results.

Python · The iterator protocol in DMarlon Studio: the example's code and its output in the terminal

A for loop knows nothing about lists: it asks the collection for an iterator with iter() and then calls next() again and again until a `StopIteration` exception appears. The program does it by hand with ["ok", "fail"]: two next() calls return both values and the third raises StopIteration, which is caught.

Any class can take part in this protocol with two special methods. __iter__ returns the iterator; in Paginator it returns self, because the object itself keeps track of where it is.

__next__ returns the next item: it slices a page out of self.items starting at self.start, moves self.start on and returns the page. When start reaches the end it raises StopIteration and the for loop ends cleanly. The last page only has two orders because slicing a list never goes past the end.

Since Paginator follows the protocol, it works with anything that loops: for, enumerate(..., start=1) to number the pages, list() to count them, sum(), comprehensions…

This iterator gets used up, just like a generator: loop over the same Paginator twice and the second time gives nothing. If you need to repeat, make __iter__ return a fresh object each time, or write __iter__ as a generator with yield.

Code · main.py

class Paginator:    """Iterates over a list of results, one page at a time."""    def __init__(self, items, page_size):        self.items = items        self.page_size = page_size        self.start = 0    def __iter__(self):        return self                   # this object is its own iterator    def __next__(self):        if self.start >= len(self.items):            raise StopIteration       # tells for that there is no more        page = self.items[self.start:self.start + self.page_size]        self.start += self.page_size        return pageorders = [f"#{n}" for n in range(1001, 1011)]# What a for loop does behind the scenes: iter() and next()it = iter(["ok", "fail"])print(next(it), next(it))try:    next(it)except StopIteration:    print("StopIteration: the list is used up")for number, page in enumerate(Paginator(orders, 4), start=1):    print(f"Page {number}: {page}")# Any function that loops accepts our iteratorprint("Pages:", len(list(Paginator(orders, 3))))

Output · python3 main.py

ok fail
StopIteration: the list is used up
Page 1: ['#1001', '#1002', '#1003', '#1004']
Page 2: ['#1005', '#1006', '#1007', '#1008']
Page 3: ['#1009', '#1010']
Pages: 4

Key points

  • for calls iter() once and next() until StopIteration.
  • __iter__ returns the iterator; __next__ gives the next value.
  • Raising StopIteration is how you say there is nothing left.
  • An iterable works with for, list(), sum(), enumerate()…

Exercise

Add a limit parameter to Paginator that stops after that many pages, even if there are items left.