How a for loop really works: the iteration protocol, iter() and next(), StopIteration and writing your own iterator class.
About 40 minIntermediate4 exercisesFree
You will be able to
Explain the difference between an iterable and an iterator
Drive iteration by hand with iter() and next(), including next()'s default value
Write a class that implements __iter__ and __next__ and ends with StopIteration
Recognise and fix bugs caused by consuming an iterator twice
You have written hundreds of for loops over lists, strings, dictionaries and files. They all look the same from the outside, yet a list keeps its items in memory while a file reads one line at a time from disk. What lets one for statement work with all of them?
The answer is a small agreement called the iteration protocol. Once you understand it you can make your own objects loopable, read the first line of a data stream and process the rest separately, and avoid one of the most confusing bugs in Python: data that silently "disappears" the second time you loop over it. Everything in the next two lessons — generators and pipelines — is built on this protocol.
Two roles: iterable and iterator
An iterable is anything you can loop over: a list, tuple, string, dict, set, range, file. Technically, it is an object whose __iter__ method returns an iterator. You ask for one with the built-in iter(obj).
An iterator is the object that does the walking. It remembers its position and hands out the next item each time you call next(it) (which calls its __next__ method). When there are no items left it raises StopIteration.
A list is iterable but is not itself an iterator: you can loop over the same list many times because each loop asks for a fresh iterator. An iterator is single-use: once it has handed out every item, it is exhausted and stays empty.
Driving iteration by hand
orders =["A-101","A-102","A-103"]
it =iter(orders)# ask the list for an iteratorprint(type(it).__name__)print(next(it))# A-101print(next(it))# A-102print(next(it))# A-103print(next(it,"no more orders"))# a default instead of StopIteration
Try it: Remove the default from the last next() call and run it again to see the StopIteration error.
What a for loop really does
This loop:
for order in orders:
print(order)
is roughly equivalent to:
it =iter(orders)whileTrue:
try:
order =next(it)except StopIteration:
breakprint(order)
The for statement calls iter() once, calls next() repeatedly, and treats StopIteration as the normal signal to stop — not as an error. sum(), list(), max(), sorted(), in, unpacking and comprehensions all use the same protocol.
Predict the output
readings =iter([12,15,11])for r in readings:
print("first loop:", r)for r in readings:
print("second loop:", r)print("done")
Using next() on purpose
Sometimes you want to treat the first item differently — a CSV header, a title line, a starting balance. Get an iterator, take the first item with next(), then loop over the rest. The loop continues from where next() left off.
Header first, then data rows
lines =["sku,qty","PEN-01,40","INK-07,12","PAD-33,5"]
rows =iter(lines)
header =next(rows)print("columns:", header.split(","))for line in rows: # continues after the header
sku, qty = line.split(",")print(sku,int(qty))
Try it: Change lines to an empty list. Which line fails, and how could next(rows, None) help?
Writing an iterator class
To make your own iterator, give a class two methods:
__iter__(self) returns the iterator — for an iterator class, that is self.
__next__(self) returns the next value, or raises StopIteration when finished.
Here is an iterator over the invoice numbers in a range, formatted as text:
An iterator class
classInvoiceNumbers:
def__init__(self, first, last):
self.current = first
self.last = last
def__iter__(self):
return self
def__next__(self):
if self.current > self.last:
raise StopIteration
number =f"INV-{self.current:04d}"
self.current +=1return number
for n in InvoiceNumbers(98,101):
print(n)
numbers = InvoiceNumbers(1,2)print(list(numbers))print(list(numbers))# exhausted: an iterator is single-use
Try it: Add a third call print(next(numbers, 'empty')) at the end.
Iterable versus iterator in your own classes
If you want an object that can be looped over many times — like a list — make it an iterable: its __iter__ returns a new iterator each time instead of self. A quick way is to return an iterator over some internal data (return iter(self.items)); in the next lesson you'll see that a generator is the neatest way.
iterable (e.g. list)
iterator (e.g. iter(list), a file)
iter(x) returns
a new iterator
itself
has __next__
no
yes
loop over it twice
works both times
second loop is empty
When not to write an iterator class: most of the time you don't need one. If the data already fits in a list, return the list. If you need lazy, one-at-a-time values, a generator function (next lesson) does the same job in a few lines. Write a class when the iterator needs extra methods or state other code inspects.
Guided practice · ●●difficulty 2 of 5 · about 6 min
Separate the header from the rows
A warehouse export arrives as an iterable of text lines — sometimes a list, sometimes a file object that can only be read once. The first line is a header.
Write split_header(lines) that returns a tuple (header, rows):
header is the first line, or None if there are no lines at all;
rows is a list of the remaining lines, in order.
Use iter() and next() so it works for any iterable, including one-shot iterators.
Python code editor. Tab indents. Press Escape then Tab to leave the editor. Control or Command plus Enter runs the code; Shift plus Enter checks your answer.
Ctrl+Enter run · Shift+Enter check · your code is saved in this browser as you type
Output
Run your code to see its output, or Check to test it against the requirements.
The solution becomes available after you pass, or after all three hints and two checks.
Challenge · ●●●difficulty 3 of 5 · about 8 min
A countdown iterator
A launch screen at a delivery depot counts down before the gates open. Write an iterator class Countdown:
Countdown(start) stores the starting number;
__iter__ returns the iterator itself;
__next__ returns start, then start - 1, … down to 1, then raises StopIteration.
Python code editor. Tab indents. Press Escape then Tab to leave the editor. Control or Command plus Enter runs the code; Shift plus Enter checks your answer.
Ctrl+Enter run · Shift+Enter check · your code is saved in this browser as you type
Output
Run your code to see its output, or Check to test it against the requirements.
The solution becomes available after you pass, or after all three hints and two checks.
Challenge · ●●●difficulty 3 of 5 · about 10 min
A reusable shift rota
A clinic assigns reception shifts in rotation. Write a class ShiftRota that is iterable (it can be looped over many times, like a list):
ShiftRota(staff, days) stores a list of names and a number of days;
looping over it yields tuples (day, name) for days 1 to days, cycling through staff in order;
every new loop starts again from day 1;
if staff is empty, or days is 0, it yields nothing.
rota = ShiftRota(["Huda","Omar"],3)list(rota)# [(1, 'Huda'), (2, 'Omar'), (3, 'Huda')]list(rota)# the same list again
Python code editor. Tab indents. Press Escape then Tab to leave the editor. Control or Command plus Enter runs the code; Shift plus Enter checks your answer.
Ctrl+Enter run · Shift+Enter check · your code is saved in this browser as you type
Output
Run your code to see its output, or Check to test it against the requirements.
The solution becomes available after you pass, or after all three hints and two checks.
Debugging · ●●●difficulty 3 of 5 · about 7 min
Fix the vanishing readings
A sensor dashboard calls summarize(readings) to get (count, average) for a batch of temperature readings. It works in the developer's tests with lists, but in production the readings arrive as an iterator streamed from a device, and the dashboard shows an average of 0.0 every time.
Fix summarize so it returns the right (count, average) for any iterable. For no readings it should return (0, None).
Python code editor. Tab indents. Press Escape then Tab to leave the editor. Control or Command plus Enter runs the code; Shift plus Enter checks your answer.
Ctrl+Enter run · Shift+Enter check · your code is saved in this browser as you type
Output
Run your code to see its output, or Check to test it against the requirements.
The solution becomes available after you pass, or after all three hints and two checks.
Summary
An iterable gives out iterators with iter(); an iterator hands out items with next() and raises StopIteration when finished.
A for loop calls iter() once and next() until StopIteration; list(), sum(), sorted() and comprehensions work the same way.
Iterators are single-use and remember their position: next() can take a header, and the loop carries on from there.
An iterator class needs __iter__ (return self) and __next__; a reusable iterable's __iter__ returns a new iterator each time.