Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 10 – Iterators, generators and functional tools

How iteration works: iterables and iterators

See what a for loop does with iter(), next() and StopIteration, write iterator classes, and stop one-shot iterators such as map and zip from emptying a loop.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free: 20 minutes

What you will learn

  • Explain what iter(), next() and StopIteration do in every for loop
  • Distinguish an iterable from an iterator, and predict when a second loop sees nothing
  • Write an iterator class, and an iterable class that every loop can restart
  • Read data in fixed-size pieces with iter(callable, sentinel)

Before you start

Read the marks "72 85 91 64" through map(int, …), add them up with sum(), then count them with len(list(marks)), and the count comes out as 0. Nothing was lost: a map hands out each value once, and sum() had already taken all four, so the average divides by zero one line later.

That zero comes from the two calls inside every for loop, iter() and next(), and from the difference between an iterable: A value a for loop can go through one item at a time, such as a string, a list, a tuple, a dict, a set or a range. A dict gives its keys. More in “for loops with range, enumerate and zip”, which hands every loop a fresh iterator: An object that hands out items one at a time: next() returns its next item, or raises StopIteration when none are left. It keeps one position, so it is used up after one pass, and iter() returns the iterator itself., and an iterator, which keeps one position and is used up after one pass. Below you will step through real runs of both, write iterator classes of both kinds, and read data in fixed-size pieces with iter(function, sentinel).

A for loop calls iter() once, then next() until StopIteration

A for loop never counts positions. It asks the object after in for an iterator by calling iter() on it, once, then calls next() on that iterator again and again, binding each result to the loop variable, until next() raises the exception StopIteration: The exception next() raises when an iterator has no items left. A for loop catches it and ends quietly, which is why a loop never shows it., which the loop treats as “no items left”. The iterator is the object that remembers the position and produces one item per next() call:

A for loop calls iter() once to get an iterator, then calls next() for every item until next() raises StopIteration.for m in marks:it = iter(marks)once, at the startnext(it)m = the itemrun the loop bodyStopIterationthe loop endsan itemagainnothing left

What a for loop does with marks

Text description of the diagram

The diagram is a flow from top to bottom with one loop in it.

  • It starts at the statement for m in marks.
  • Once, at the start, the loop calls iter(marks) and keeps the iterator it returns, here called it.
  • It then calls next(it). If next() returns an item, the item is assigned to m and the loop body runs, and the flow goes back to next(it) for the next item.
  • When there is nothing left, next() raises StopIteration, and the loop ends without showing the exception.

The program below runs both versions over the same three marks: first a for statement, then the same loop spelled out by hand with the three steps that the statement performs for you. Step through it and watch the flag under marks, the position the next item comes from:

Interactive · line by line One list, read by a for loop and then by hand Step through both loops. The flag under marks is the position the next item comes from; in the second loop it is the iterator named it.

Program · for_by_hand.py

marks = [72, 85, 91]

for m in marks:
    print("for:", m)

it = iter(marks)
while True:
    try:
        item = next(it)
    except StopIteration:
        break
    print("by hand:", item)

print(type(it).__name__, iter(it) is it)

Variables

After line 14: marks is [72, 85, 91]; m is 91; it is <list_iterator object>; item is 91.marks720851912m91it<list_iterator object>item91
  • just taken by the loop
  • changed in this step
  • passed by the loop
  • read next
  • where the loop stopped

Printed so far

for: 72
for: 85
for: 91
by hand: 72
by hand: 85
by hand: 91
list_iterator True

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 28 steps are listed below.

Line run 14
Next line end
Loop pass –
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. marks = [72, 85, 91] makes marks a list of 3 items.
  3. Line 3. for m in marks: takes the item at position 0 of marks: m is 72. The for statement called iter(marks) once, before this first pass, and keeps that iterator out of sight. The flag under marks shows where it reads next.
  4. Line 4. print("for:", m) prints for: 72.
  5. Line 3. for m in marks: takes the item at position 1 of marks: m is 85.
  6. Line 4. print("for:", m) prints for: 85.
  7. Line 3. for m in marks: takes the item at position 2 of marks: m is 91.
  8. Line 4. print("for:", m) prints for: 91.
  9. Line 3. for m in marks: finds no item at position 3 of marks (it has 3 items), so the loop ends.
  10. Line 6. it = iter(marks) sets it to <list_iterator object>. iter(marks) makes a new iterator, a separate object that keeps the position. The flag labelled it is that position: 0, the first mark.
  11. Line 7. while True: tests its condition: it is true, so the body runs.
  12. Line 8. try: runs.
  13. Line 9. item = next(it) sets item to 72.
  14. Line 12. print("by hand:", item) prints by hand: 72.
  15. Line 7. while True: tests its condition: it is true, so the body runs again (pass 2).
  16. Line 8. try: runs.
  17. Line 9. item = next(it) sets item to 85 (it was 72).
  18. Line 12. print("by hand:", item) prints by hand: 85.
  19. Line 7. while True: tests its condition: it is true, so the body runs again (pass 3).
  20. Line 8. try: runs.
  21. Line 9. item = next(it) sets item to 91 (it was 85).
  22. Line 12. print("by hand:", item) prints by hand: 91.
  23. Line 7. while True: tests its condition: it is true, so the body runs again (pass 4).
  24. Line 8. try: runs.
  25. Line 9. item = next(it) item keeps its value, 91. Key point There is no item at position 3, so next(it) raises StopIteration instead of returning a value, and item keeps 91.
  26. Line 10. Line 10 runs. This is the step a for loop does for you. It catches StopIteration and ends, which is why a for loop never shows the exception.
  27. Line 11. break leaves the loop at once, without reading the items it has not reached.
  28. Line 14. print(type(it).__name__, iter(it) is it) prints list_iterator True. That was the last line, so the program ends. Both loops gave 72, 85 and 91 in order. iter(it) gave back it itself, which is why an iterator can stand wherever an iterable is expected.

The while loop is what Python does underneath. iter(marks) runs once and returns a list_iterator, a separate object that keeps the position. Each next(it) returns the following item, and after the last one next() raises StopIteration. A for statement catches that exception itself, which is why you never see it. The last line shows the rule that makes iterators special: calling iter() on an iterator returns the very same object.

Checkpoint

Check: iter() once, next() until StopIteration

  1. Question 1 of 2 How many times does for m in marks: call iter() on marks, when marks holds 3 items?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Once, before the first pass

    A for loop calls iter() once, at the start, and then next() once per item and once more: the call that raises StopIteration and ends the loop.

  2. Question 2 of 2 for m in marks: print(m) runs over marks = [72, 85, 91]. How many times is next() called on its iterator?

    Type a number.

    A hint

    Count the calls that return an item, then the one that finds there is none.

    Show the answer: Question 2 of 2

    Answer: 4 calls

    Three calls return 72, 85 and 91. A fourth call finds no item at position 3 and raises StopIteration, which ends the loop, just as next(it) did on the last pass of the traced while loop.

A list hands out new iterators; an iterator keeps one position

Anything that can hand out an iterator is an iterable: lists, strings, dictionaries, ranges and files are all iterables. An iterable has an __iter__() method that returns an iterator. An iterator has a __next__() method that returns the next item or raises StopIteration, plus an __iter__() method that returns the iterator itself, so that an iterator can be used wherever an iterable is expected. Step through two iterators over one list, and a second name for one of them:

Interactive · line by line Two iterators over one list, and one iterator with two names Watch the flags under marks. Each iter(marks) makes an iterator with a position of its own; iter(first) hands back first itself.

Program · two_iterators.py

marks = [72, 85, 91]
first = iter(marks)
second = iter(marks)
print(next(first), next(first))
print(next(second))
same = iter(first)
print(same is first, next(same))
print(next(first, "empty"))

Variables

After line 8: marks is [72, 85, 91] (the loop reads position 1 next); first is <list_iterator object>; second is <list_iterator object>; same is <list_iterator object>.marks720851912secondfirst<list_iterator object>second<list_iterator object>same<list_iterator object>
  • changed in this step
  • passed by the loop
  • read next

Printed so far

72 85
72
True 91
empty

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 9 steps are listed below.

Line run 8
Next line end
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. marks = [72, 85, 91] makes marks a list of 3 items.
  3. Line 2. first = iter(marks) sets first to <list_iterator object>.
  4. Line 3. second = iter(marks) sets second to <list_iterator object>. first and second are two iterators over the same list. Each keeps its own position, so both flags start at 0.
  5. Line 4. print(next(first), next(first)) prints 72 85. Two calls of next(first) move only the flag of first, to position 2. second has not moved.
  6. Line 5. print(next(second)) prints 72. second gives the first mark, 72, as every new iterator over a list does.
  7. Line 6. same = iter(first) sets same to <list_iterator object>. Key point For an iterator, iter() returns the iterator itself, so same is not a new position: it is a second name for first.
  8. Line 7. print(same is first, next(same)) prints True 91. So next(same) moves the flag of first. It gives 91, the item at position 2.
  9. Line 8. print(next(first, "empty")) prints empty. That was the last line, so the program ends. first has nothing left, so next() returned its default. The list can still hand out a new iterator at any time; an iterator, once used up, stays used up.

Every container makes a new iterator each time you ask, so two loops over a list never disturb each other: the flags of first and second moved on their own. An iterator has only one position, so every loop over it, and every name for it, continues where the previous one stopped.

Iterable or iterator?
Criterion IterableIterator
Methods __iter__(), which returns a new iterator__next__(), and __iter__() returning itself
Loop over it twice Both loops see every itemThe second loop sees what the first left
len() and indexing Often, as in lists and rangesNot in the built-in ones
Examples list, tuple, str, dict, set, rangemap, filter, zip, enumerate, reversed, files
When to choose Data you read many timesOne pass over data produced as you go

The program below loops twice over eleven common objects and reports what the second loop saw:

Which objects survive a second loop Python · one_shot_or_not.py
import io

things = {
    "list": [1, 2],
    "tuple": (1, 2),
    "str": "ab",
    "dict": {"a": 1, "b": 2},
    "range": range(2),
    "map": map(str, [1, 2]),
    "filter": filter(None, [1, 0, 2]),
    "zip": zip("ab", "cd"),
    "enumerate": enumerate("ab"),
    "reversed": reversed([1, 2]),
    "file": io.StringIO("line 1\nline 2\n"),
}
for name, obj in things.items():
    first = list(obj)
    second = list(obj)
    kind = "iterator" if iter(obj) is obj else "iterable"
    print(f"{name:10} {kind:9} second loop: {len(second)} of {len(first)} items")

Output

list       iterable  second loop: 2 of 2 items
tuple      iterable  second loop: 2 of 2 items
str        iterable  second loop: 2 of 2 items
dict       iterable  second loop: 2 of 2 items
range      iterable  second loop: 2 of 2 items
map        iterator  second loop: 0 of 2 items
filter     iterator  second loop: 0 of 2 items
zip        iterator  second loop: 0 of 2 items
enumerate  iterator  second loop: 0 of 2 items
reversed   iterator  second loop: 0 of 2 items
file       iterator  second loop: 0 of 2 items

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 one_shot_or_not.py

The pattern is exact: every object that is its own iterator came back empty the second time. The documentation of iterator types adds a rule for the end of an iterator. Once __next__() has raised StopIteration, it must keep raising it on every later call; an iterator that starts producing items again is called broken.

Key idea

An iterable hands out a new iterator every time iter() is called on it. An iterator returns itself from iter(), so it has one position and is used up after one pass.

Checkpoint

Check: Iterable or iterator

  1. Question 1 of 2 With marks = [72, 85, 91], a program runs a = iter(marks), b = iter(marks) and next(a). What does next(b) return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 72

    A list hands out a new iterator for every call of iter(), so a and b move on their own, as the flags of first and second did in the figure.

  2. Question 2 of 2 Which test is True for an iterator but False for a list?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: iter(obj) is obj

    iter(obj) is obj is the test one_shot_or_not.py uses: True for map, zip, files and every other iterator, False for lists, tuples, strings, dictionaries and ranges.

Every read moves an iterator forward, so a second loop sees what is left

The difference matters because many everyday objects, map(), zip(), filter(), enumerate() and open files among them, are iterators themselves, and anything that reads an iterator moves it on, not only a for loop. To watch it happen, wrap a sequence in a class that counts the calls to next():

How many times next() is called Python · counting_calls.py
class Counted:
    """Wraps an iterable and counts how often next() is called on it."""

    def __init__(self, items):
        self._it = iter(items)
        self.calls = 0

    def __iter__(self):
        return self

    def __next__(self):
        self.calls += 1
        return next(self._it)   # raises StopIteration at the end


c = Counted("abcde")
for ch in c:
    pass
print("a for loop over 5 items:", c.calls, "calls to next()")

c = Counted(range(1_000_000))
print("999_999 in c:", 999_999 in c, "after", c.calls, "calls")

c = Counted(range(1_000_000))
print("3 in c:", 3 in c, "after", c.calls, "calls")
print("list(c) then holds", len(list(c)), "items")

Output

a for loop over 5 items: 6 calls to next()
999_999 in c: True after 1000000 calls
3 in c: True after 4 calls
list(c) then holds 999996 items

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 counting_calls.py

  • A loop over 5 items calls next() 6 times: one call for each item, and one more that raises StopIteration.
  • 999_999 in c had to pull a million items before it found the last one, because in on an iterator has nothing better to do than call next() and compare.
  • 3 in c stopped after 4 calls, and those 4 items were gone: the list() that followed collected only the 999,996 items that were left. A membership test on an iterator moves it forward.

The bug this causes is rarely a crash at the point of the mistake. Here sum() reads every number from a map object, and the line that counts them gets an iterator that is already exhausted:

An average that divides by zero Python · marks_average.py
line = "72 85 91 64"

marks = map(int, line.split())
total = sum(marks)
count = len(list(marks))       # the map was used up by sum()
print("total:", total, "count:", count)
try:
    print("average:", total / count)
except ZeroDivisionError as e:
    print("ZeroDivisionError:", e)

marks = list(map(int, line.split()))   # a list can be read again
print("average:", sum(marks) / len(marks))

Output

total: 312 count: 0
ZeroDivisionError: division by zero
average: 78.0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 marks_average.py

count is 0 because list(marks) started where sum() stopped, at the end. Step through the two reads and watch the map’s position:

Interactive · message by message sum() and list() read the same map, one after the other Step through total = sum(marks) and count = len(list(marks)). The arrows are calls on the map and what it hands back.
sum() and list() read the same map, one after the other, step 8 of 8. Messages so far: sum(marks) to marks: iter(marks); marks to sum(marks): 72; marks to sum(marks): 85, 91, 64; marks to sum(marks): StopIteration; len(list(marks)) to marks: iter(marks); marks to len(list(marks)): StopIteration.sum(marks)returned 312marksa list: [72, 85, 91, 64]len(list(marks))count 4iter(marks)7285, 91, 64StopIterationiter(marks)StopIteration

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. sum() asks for an iterator. sum(marks) first calls iter(marks). A map object is an iterator, so iter() hands back the map itself, still before the first part of the line, '72'.
  2. The first mark. Each next() makes the map call int() on the next part of the line. The first call turns '72' into 72, and the map's position moves past it.
  3. Three more. Three more calls hand over 85, 91 and 64. sum() adds each one as it arrives and keeps none of them.
  4. The end of the map. The fifth next() finds no part left, so the map raises StopIteration. sum() takes that as the end and returns 312.
  5. Counting starts. list(marks) calls iter(marks) as well and gets the same map object, which still has nothing left.
  6. Nothing to count. The first next() raises StopIteration at once, so list() returns an empty list and len() gives 0.
  7. The error, one line later. total / count is 312 / 0, which raises ZeroDivisionError. The mistake happened a line earlier, when one iterator was read twice.
  8. The fix. list(map(int, line.split())) stores the four marks once. A list hands every reader a new iterator, so sum() and len() both see all four, and the average is 78.0.

The error appears one line later, as a division by zero, far from its cause. The fix is to decide how many times the data will be read. If more than once, store it in a list first. If the data is too big for a list, compute everything in one pass, keeping a running total and a running count in the same loop.

Reading a map, a zip or a file twice

total = sum(marks) followed by len(list(marks)) reads one iterator twice, and the second read sees nothing. Write marks = list(map(int, parts)) when the data is read more than once, or add up and count in one loop when it is too big for a list.

Checkpoint

Check: Reading an iterator twice

  1. Question 1 of 2 it = iter(range(10)), and then 3 in it returns True. How many items does list(it) return after that?

    Type a number.

    A hint

    in calls next() until it finds 3. Which numbers has it taken by then?

    Show the answer: Question 1 of 2

    Answer: 6 items

    in took 0, 1, 2 and 3 from the iterator, comparing each with 3, and stopped at the match. Those four are gone, so list(it) gets the six numbers that are left, 4 to 9.

  2. Question 2 of 2 A report needs the largest and the smallest of readings = map(float, row.split()). Which code gives both?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: values = list(readings), then max(values) and min(values)

    Data that is read twice has to be stored first. A list hands each reader a new iterator; a map has one position, which the first reader moves to the end.

An iterator class keeps its position in its attributes

Any class with those two methods works with for, list(), sum(), in and every other tool that iterates. Countdown below counts down from a start to 1: __next__() returns the current number and moves on, and raises StopIteration once it reaches 0. Its __repr__() lets the figure show the attribute that holds the position, so step through and watch c:

Interactive · line by line A Countdown iterator keeps its position in an attribute Watch c. Its repr shows the attribute current, which __next__() lowers by one every time the loop asks for an item.

Program · countdown.py

class Countdown:
    """An iterator: counts down from start to 1, once."""

    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        value = self.current
        self.current -= 1
        return value

    def __repr__(self):
        return f"Countdown(current={self.current})"


c = Countdown(3)
for n in c:
    print(n)
print(list(c))
print(next(c, "done"))

Variables

After line 25: Countdown is Countdown; c is Countdown(current=0); n is 1.CountdownCountdowncCountdown(current=0)n1
  • changed in this step

Printed so far

3
2
1
[]
done

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 12 steps are listed below.

Line run 25
Next line end
Loop pass –
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. class Countdown: makes the class Countdown. Its body runs only when it is used. The class now holds its methods. __iter__() and __next__() run only when something asks c for items.
  3. Line 21. c = Countdown(3) sets c to Countdown(current=3). Creating c runs __init__(), which stores 3 in c.current. Nothing has been counted yet.
  4. Line 22. for n in c: starts: n is 3. The loop called iter(c), which returned c itself, then next(c): __next__() returned 3 and lowered current to 2.
  5. Line 23. print(n) prints 3.
  6. Line 22. for n in c: goes round again: n is 2.
  7. Line 23. print(n) prints 2.
  8. Line 22. for n in c: goes round again: n is 1.
  9. Line 23. print(n) prints 1.
  10. Line 22. for n in c: has no item left, so the loop ends. Key point current is 0, so __next__() raised StopIteration and the loop ended. The position lives in the object, and it stays at 0.
  11. Line 24. print(list(c)) prints []. list(c) asks the same object again. current is still 0, so the first next() raises StopIteration: an empty list.
  12. Line 25. print(next(c, "done")) prints done. That was the last line, so the program ends. With a second argument, next() returns that default instead of raising StopIteration.

The position is the attribute current, which __next__() lowers on every pass. Because the class returns self from __iter__(), a Countdown object behaves like map: once the loop has used it up, list(c) is empty. The last line shows next() with a second argument, a default that it returns instead of raising StopIteration. Press Edit, change the start to 5 and run it again: the loop prints five numbers, and the list after it is still empty.

Checkpoint

Check: An iterator class

  1. One question Countdown is the class of countdown.py. What does this print?

    Read the code, then choose one answer.

    c = Countdown(2)
    print(next(c), list(c), list(c))
    Show the answer: One question

    Answer: 2 [1] []

    All three reads share the attribute current: next() takes 2, the first list() takes what is left, [1], and the second list() finds nothing.

An iterable class gives every loop its own iterator

A collection that people loop over more than once should not be its own iterator. Make __iter__() return a new iterator object instead, so that every loop gets its own position, nested loops included. reusable.py runs the same nested loops over a one-shot iterator and over such an iterable. Its OneShot is the iterator class of the previous section under a new name, and its Countdown is a different class, the iterable built on it:

Nested loops over a one-shot iterator and over an iterable Python · reusable.py
class OneShot:
    """An iterator: every loop shares the same position."""

    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


class Countdown:
    """An iterable: every loop gets a new iterator of its own."""

    def __init__(self, start):
        self.start = start

    def __iter__(self):
        return OneShot(self.start)


for make in (OneShot, Countdown):
    launch = make(3)
    pairs = [(a, b) for a in launch for b in launch if a > b]
    print(f"{make.__name__:9}", pairs)

Output

OneShot   [(3, 2), (3, 1)]
Countdown [(3, 2), (3, 1), (2, 1)]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 reusable.py

With OneShot, the outer loop took 3, and the inner loop then took 2 and 1 from the same object, leaving nothing for the outer loop: only two pairs. Countdown stores just the starting number and creates a fresh OneShot each time iter() is called, so the inner loop starts again at 3 for every outer item and the result has all three pairs. Step through both runs:

Interactive · message by message Two loops over OneShot(3), then over Countdown(3) Step through the two loops of reusable.py. launch is in the middle; the arrows are the loops asking it for an iterator or an item.
Two loops over OneShot(3), then over Countdown(3), step 11 of 11. Messages so far: for a in launch to launch: iter(launch); launch to for a in launch: 3; for b in launch to launch: iter(launch); launch to for b in launch: 2, then 1; launch to for b in launch: StopIteration; launch to for a in launch: StopIteration; for a in launch to launch: iter(launch); launch to for a in launch: 3; for b in launch to launch: iter(launch); launch to for a in launch: 2; launch to for a in launch: StopIteration.for a in launchall 3 pairslaunchCountdown(3): start = 3for b in launch(2, 1) as welliter(launch)3iter(launch)2, then 1StopIterationStopIterationiter(launch)3iter(launch)2StopIteration

The drawing shows the whole scene; its 11 steps are listed below.

Every step as text
  1. The outer loop starts. The outer loop calls iter(launch). OneShot returns itself, so this loop reads the object's own position.
  2. a = 3. next() gives 3, so a is 3, and the one position moves on to 2.
  3. The inner loop starts. For a = 3 the inner loop calls iter(launch) too. It gets the same object with the same position, so it starts at 2, not at 3.
  4. Two pairs. The inner loop takes 2 and then 1, and keeps (3, 2) and (3, 1). The one position is now past the end.
  5. The inner loop ends. The next call raises StopIteration, which ends the inner loop.
  6. The outer loop ends too. The outer loop asks for its next item and gets StopIteration as well, because the inner loop used the items up. OneShot gives only 2 pairs.
  7. Now Countdown(3). Countdown stores only its start. Each iter(launch) builds a new OneShot(3), so the outer loop gets a position of its own.
  8. a = 3 again. The outer loop's own iterator gives 3.
  9. A new inner iterator. The inner loop's iter(launch) builds a second OneShot(3), which starts again at 3. It gives 3, 2 and 1, and the loop keeps (3, 2) and (3, 1).
  10. a = 2. The inner loop never touched the outer loop's iterator, so it gives 2. A third new OneShot(3) lets the inner loop find (2, 1).
  11. Three pairs. For a = 1 the inner loop finds nothing smaller, then the outer iterator ends. Every loop had a position of its own, so Countdown gives all 3 pairs.

Lists, dictionaries and ranges are built exactly this way. The next lesson replaces such iterator classes with a single function that uses yield, which saves writing the class at all.

Key idea

Return self from __iter__() only in an iterator. A collection returns a new iterator from __iter__(), so every loop over it, nested or not, starts at the beginning.

Checkpoint

Check: An iterable that restarts

  1. Question 1 of 2 With launch = OneShot(4), the one-shot class of reusable.py, how many pairs does [(a, b) for a in launch for b in launch if a > b] make?

    Type a number.

    A hint

    The outer loop takes 4. What does the inner loop leave for it after that?

    Show the answer: Question 1 of 2

    Answer: 3 pairs

    The outer loop takes 4, and the inner loop reads the same position: 3, 2 and 1, giving (4, 3), (4, 2) and (4, 1). Then the object is used up, so the outer loop ends too. With Countdown(4) every loop has its own iterator and the comprehension makes 6 pairs.

  2. Question 2 of 2 A Shelf class keeps its books in the list self.books. Which __iter__() lets two nested loops over one shelf each start at the first book?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: return iter(self.books)

    A collection returns a new iterator from __iter__(), here the one the list makes. Countdown does the same by building a new OneShot, and lists, dictionaries and ranges do it themselves.

range computes its numbers, so two billion take no more memory than three

range() stores only its start, stop and step, and computes numbers when they are needed. Its length, its items by position and membership tests come from arithmetic, so a range of two billion numbers costs no more memory than range(3):

A range of two billion numbers Python · range_lazy.py
import sys

r = range(0, 10**10, 5)        # two billion numbers: every fifth one below ten billion

print(len(r), r[3], r[-1])     # length and indexing, computed
print(9_999_999_995 in r, 7 in r)      # membership by arithmetic, not by looping
print(sys.getsizeof(r) == sys.getsizeof(range(3)))   # the same size in memory
print(list(range(3)), list(range(3)))  # loops over it twice

it = iter(range(3))            # an iterator over a range is one-shot
print(type(it).__name__, list(it), list(it))

Output

2000000000 15 9999999995
True False
True
[0, 1, 2] [0, 1, 2]
range_iterator [0, 1, 2] []

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 range_lazy.py

sys.getsizeof() reports the same size for both ranges, the third line’s True. The membership test for 9,999,999,995 answered at once: for integers, CPython computes it from the start, stop and step instead of looping. And because a range is an iterable, not an iterator, it can be looped over any number of times. Only iter(range(3)), a range_iterator, behaves like the one-shot objects above.

Checkpoint

Check: range, a lazy sequence

  1. One question Which code prints [0, 1, 2] []?

    Choose one answer.

    Show the answer: One question

    Answer: it = iter(range(3)), then print(list(it), list(it))

    A range can be looped over any number of times, because it hands out a new iterator for every loop. Only an iterator over it, such as iter(range(3)), is used up after one pass.

iter(function, sentinel) calls a function until it returns the sentinel

iter() has a second form. Given a function and a sentinel: A value that marks the end of the data instead of a count, such as the b"" that read() returns at the end of a file. iter(func, sentinel) calls func() until it returns that value. value, iter(function, sentinel) returns an iterator that calls the function with no arguments on every next() and stops as soon as it returns the sentinel. Reading a binary file in fixed-size pieces is its classic use, because read() returns b"" at the end:

Reading 4 bytes at a time, and rolling dice until a 6 Python · chunks.py
import io
import random
from functools import partial

data = io.BytesIO(b"0123456789abcdef!")
read4 = partial(data.read, 4)        # read4() returns the next 4 bytes
for chunk in iter(read4, b""):       # call read4() until it returns b""
    print(chunk)

rng = random.Random(7)
roll = partial(rng.randint, 1, 6)
print("rolls before the first 6:", list(iter(roll, 6)))

Output

b'0123'
b'4567'
b'89ab'
b'cdef'
b'!'
rolls before the first 6: [3, 2, 4]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 chunks.py

functools.partial(data.read, 4) makes a function that needs no arguments, which is what this form of iter() requires. Step through the first loop and watch each result meet the sentinel test:

Interactive · step by step iter(read4, b"") calls read4() until it returns b"" Step through the first loop of chunks.py. Each next() calls read4() once, and the result goes to the loop unless it equals the sentinel.
iter(read4, b"") calls read4() until it returns b"", step 7 of 7. Showing for chunk in iter(…) (got [3, 2, 4]), read4() (roll() this time), equal to b""? (equal to 6?), StopIteration.next()the resultno: the chunkyesfor chunk in iter(…)got [3, 2, 4]read4()roll() this timeequal to b""?equal to 6?StopIterationthe loop ends

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. next() calls read4(). The loop asks the iterator for its first item. The iterator calls read4() with no arguments, which reads the next 4 bytes of data.
  2. Not the sentinel. read4() returns b'0123'. The iterator compares it with the sentinel, b"", and they are not equal.
  3. The loop gets it. So next() returns b'0123', the loop gives it the name chunk, and its body prints it.
  4. Three more chunks. The same happens three more times: b'4567', b'89ab' and b'cdef' each pass the test and are printed.
  5. The last byte. Only 1 byte is left, so read4() returns b'!'. That is still not b"", so the loop gets it as well.
  6. The sentinel. Now read4() returns b"", the end of the data. It equals the sentinel, so the iterator raises StopIteration and the loop ends; b"" never reaches the loop.
  7. Any value can end it. The dice use the same rule with iter(roll, 6). The rolls 3, 2 and 4 come out, and the first 6 stops the iterator without being returned.

The loop received 4 bytes at a time and the 1 byte left at the end, then stopped at b"" without passing it on. The dice show that the sentinel can be any value: the rolls before the first 6 were 3, 2 and 4, and the 6 itself was not returned. The random generator is seeded, so the run gives the same rolls every time.

Checkpoint

Check: iter() with a sentinel

  1. One question data = io.BytesIO(b"abcdefghij") holds 10 bytes. How many times does the body of for chunk in iter(partial(data.read, 4), b""): run?

    Type a number.

    A hint

    Write down what each read(4) returns until it returns b"".

    Show the answer: One question

    Answer: 3 times

    read(4) returns b"abcd", b"efgh" and b"ij", and the body runs for each of them. The fourth call returns b"", the sentinel, so the iterator raises StopIteration and that value never reaches the loop: 4 calls, 3 passes.

Python Online Compiler Paste the classes of countdown.py and reusable.py into a full editor and add print() calls inside __next__() to watch every call.

Practise: a Fibonacci iterator and overlapping pairs

Write two iterators with sample tests that run in your browser and a first hint: a Fibonacci class that is used up after one pass, and a pairs() function that stays lazy on input that can be read only once.

Exercise · Medium · Python

A Fibonacci iterator and overlapping pairs

Write two iterators in sequences.py, without itertools and without yield (generators come in the next lesson).

  1. Fibonacci(limit=None) is an iterator class. It produces the Fibonacci numbers 0, 1, 1, 2, 3, 5, 8 … and stops after the last number that is not greater than limit, so list(Fibonacci(10)) is [0, 1, 1, 2, 3, 5, 8]. Without a limit it never stops. Like every iterator it is used up once: a second list() of the same object is [], and next() keeps raising StopIteration.
  2. pairs(iterable) returns an iterator of the overlapping pairs of neighbouring items: list(pairs("abcd")) is [("a", "b"), ("b", "c"), ("c", "d")]. It must work on any iterable, including iterators that can be read only once, and it must be lazy: asking for the first pair reads only the first two items. Fewer than two items give no pairs.

Starter code · sequences.py

class Fibonacci:
    """The Fibonacci numbers 0, 1, 1, 2, 3, ... up to limit (None: no end)."""

    def __init__(self, limit=None):
        self.limit = limit

    def __iter__(self):
        return self

    def __next__(self):
        raise StopIteration


def pairs(iterable):
    """An iterator of the overlapping pairs of neighbouring items."""
    items = list(iterable)
    return iter([(items[i], items[i + 1]) for i in range(len(items) - 1)])
The sample tests · test_sequences.py
from itertools import islice

from sequences import Fibonacci, pairs


class Reader:
    """An iterator over items that counts how many were read."""

    def __init__(self, items):
        self._it = iter(items)
        self.read = 0

    def __iter__(self):
        return self

    def __next__(self):
        value = next(self._it)
        self.read += 1
        return value


def test_fibonacci_with_limit():
    """stops after the last number that is not greater than the limit"""
    assert list(Fibonacci(10)) == [0, 1, 1, 2, 3, 5, 8]
    assert list(Fibonacci(13)) == [0, 1, 1, 2, 3, 5, 8, 13]
    assert list(Fibonacci(0)) == [0]


def test_fibonacci_without_limit():
    """keeps going without a limit"""
    assert list(islice(Fibonacci(), 12)) == [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]


def test_fibonacci_is_one_shot():
    """is its own iterator and stays used up"""
    f = Fibonacci(5)
    assert iter(f) is f
    assert list(f) == [0, 1, 1, 2, 3, 5]
    assert list(f) == []
    assert next(f, "done") == "done"


def test_pairs():
    """pairs neighbouring items, and gives nothing for fewer than two items"""
    assert list(pairs("abcd")) == [("a", "b"), ("b", "c"), ("c", "d")]
    assert list(pairs([1, 2])) == [(1, 2)]
    assert list(pairs([1])) == []
    assert list(pairs([])) == []


def test_pairs_is_lazy():
    """reads only the items it needs, from an iterator that can be read once"""
    source = Reader(range(1_000_000))
    p = pairs(source)
    assert next(p) == (0, 1)
    assert source.read == 2
    assert next(p) == (1, 2)
    assert source.read == 3
    assert iter(p) is p
A hint

An iterator class needs two methods: __iter__(), which returns self, and __next__(), which returns the next value or raises StopIteration. For Fibonacci, keep the two latest numbers as attributes and move them on in __next__(). For pairs(), write a small class that calls iter() on the iterable once in __init__() and remembers the previous item; pairs() returns an instance of it.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Last, a session of questions on the seven ideas above. It opens with the idea you have answered least well so far and explains every answer, right or wrong, as soon as you give it:

Practice that adapts to you

Practise this lesson

14 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 14 What does a for loop do when next() raises StopIteration?

    Choose one answer.

    Show the answer: Question 1 of 14

    Answer: It ends quietly, and runs its else block if it has one

    A for loop calls next() until StopIteration and treats that exception as "no items left": it ends, runs its else block if there is one, and the program goes on.

  2. Question 2 of 14 Which while loop does exactly what for m in marks: print(m) does, for any iterable marks?

    Choose one answer.

    Show the answer: Question 2 of 14

    Answer: it = iter(marks), then while True: with try: m = next(it), except StopIteration: break and print(m)

    A for statement calls iter() once and then next() until StopIteration, which it catches: exactly the loop written out in for_by_hand.py, and it works on any iterable.

  3. Question 3 of 14 What does iter(it) return when it is already an iterator?

    Choose one answer.

    Show the answer: Question 3 of 14

    Answer: it itself, the same object

    iter() of an iterator returns the same object. That is why same = iter(first) gave a second name for first, not a new position.

  4. Question 4 of 14 Which of these objects are iterators, so that a second loop over the same object sees nothing? Choose every right answer.

    Choose every answer that is right.

    Show the answer: Question 4 of 14

    Answer:

    • map(int, parts)
    • zip(names, marks)
    • An open text file

    map, zip and file objects return themselves from iter(), so every loop continues where the last one stopped. range is a sequence that computes its numbers, and a dictionary is a container: each loop over them gets a new iterator and starts from the beginning.

  5. Question 5 of 14 Why did count come out as 0 in marks_average.py?

    Choose one answer.

    Show the answer: Question 5 of 14

    Answer: sum() had read the map to its end, and list() started from there

    sum(marks) read 72, 85, 91 and 64 and left the map at its end. list(marks) asked the same map for items, got StopIteration at once and returned [], so len() gave 0.

  6. Question 6 of 14 What does this print?

    Read the code, then choose one answer.

    it = iter([1, 2, 3, 4])
    print(2 in it, list(it))
    Show the answer: Question 6 of 14

    Answer: True [3, 4]

    in on an iterator calls next() until it finds the value, so it reads 1 and 2 and stops there. The items it read are gone: list() starts from the current position and gets only 3 and 4.

  7. Question 7 of 14 In an iterator class such as Countdown, what must __iter__() return?

    Choose one answer.

    Show the answer: Question 7 of 14

    Answer: self, the iterator itself

    An iterator class needs __iter__() returning self and __next__() returning the next item or raising StopIteration.

  8. Question 8 of 14 What does this print?

    Read the code, then choose one answer.

    it = iter("ab")
    print(next(it), next(it), next(it, "end"), next(it, "end"))
    Show the answer: Question 8 of 14

    Answer: a b end end

    The first two calls return the two characters. After that the iterator is used up, and every further call would raise StopIteration; with a default, next() returns the default instead, as often as it is called.

  9. Question 9 of 14 Countdown in reusable.py stores only its start. What does its __iter__() do on every call?

    Choose one answer.

    Show the answer: Question 9 of 14

    Answer: It returns a new OneShot(self.start)

    Countdown is an iterable: every call of iter() builds a new OneShot that starts at the stored number, so every loop has a position of its own.

  10. Question 10 of 14 What does reusable.py print?

    What does this program print? Choose one answer.

    class OneShot:
        """An iterator: every loop shares the same position."""
    
        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
    
    
    class Countdown:
        """An iterable: every loop gets a new iterator of its own."""
    
        def __init__(self, start):
            self.start = start
    
        def __iter__(self):
            return OneShot(self.start)
    
    
    for make in (OneShot, Countdown):
        launch = make(3)
        pairs = [(a, b) for a in launch for b in launch if a > b]
        print(f"{make.__name__:9}", pairs)
    Show the answer: Question 10 of 14

    Answer: it prints

    OneShot   [(3, 2), (3, 1)]
    Countdown [(3, 2), (3, 1), (2, 1)]

    With OneShot, both loops share one position. The outer loop takes 3, the inner loop takes 2 and 1 and uses the iterator up, so the outer loop has nothing left after its first item. Countdown hands every loop a new iterator, so the inner loop starts again at 3 for every outer item.

  11. Question 11 of 14 What does range(0, 10**10, 5) keep in memory?

    Choose one answer.

    Show the answer: Question 11 of 14

    Answer: Its start, stop and step

    A range stores its start, stop and step, and works out its length, its items and membership tests from them.

  12. Question 12 of 14 How many numbers does range(0, 10**10, 5) hold, as len() reports it?

    Type a number.

    A hint

    Every fifth number from 0 up to, but not including, ten billion.

    Show the answer: Question 12 of 14

    Answer: 2000000000 numbers

    Ten billion divided by a step of 5 is two billion, which len() computes from the start, stop and step: 2,000,000,000, the first number range_lazy.py printed.

  13. Question 13 of 14 iter(read4, b"") is given a function and a second argument. What does the iterator it returns do?

    Choose one answer.

    Show the answer: Question 13 of 14

    Answer: It calls read4() for every item and stops as soon as a call returns b""

    With two arguments, iter(callable, sentinel) calls the function with no arguments each time next() is called and returns what it got, until the result equals the sentinel; the sentinel itself is not returned.

  14. Question 14 of 14 Which loop header reads the binary file f 64 bytes at a time until its end?

    Choose one answer.

    Show the answer: Question 14 of 14

    Answer: for block in iter(partial(f.read, 64), b""):

    iter(callable, sentinel) calls a function with no arguments. partial(f.read, 64) is such a function, and b"" is the value read() returns at the end of the file.

Interview questions

Fresher to 2–5 years · What is the difference between an iterable and an iterator?

An iterable is any object that can produce an iterator: calling iter() on it works, because it defines __iter__() (or, in older code, __getitem__() with indexes from 0). Lists, strings, dictionaries, sets, ranges and files are iterables. An iterator is the object that does the iterating: it has __next__(), which returns the next item or raises StopIteration when there are no more, and an __iter__() that returns itself. The practical difference is reuse. A container gives every loop a new iterator, so it can be looped over again and again, while an iterator has a single position and is used up after one pass. That is why map(), zip(), filter() and open files give an empty second loop. Every iterator is an iterable, but not every iterable is an iterator.

Key takeaways

  • for x in obj calls iter(obj) once, then next() until StopIteration, which the loop handles silently.
  • An iterable hands out a new iterator to every loop; an iterator returns itself from iter(), keeps one position and is used up after one pass, as map, filter, zip, enumerate, reversed and files are.
  • Every read moves an iterator forward, in and sum() included, so a second loop over the same iterator sees only what is left: keep a list when the data is read more than once.
  • Write an iterator class with __iter__() returning self and __next__() raising StopIteration; for data that is looped over more than once, return a new iterator from __iter__() instead.
  • range is a lazy sequence that computes its numbers and can be looped over many times; iter(function, sentinel) calls a function until it returns the sentinel.

Terms in this lesson

iterator
An object that hands out items one at a time: next() returns its next item, or raises StopIteration when none are left. It keeps one position, so it is used up after one pass, and iter() returns the iterator itself.
sentinel
A value that marks the end of the data instead of a count, such as the b"" that read() returns at the end of a file. iter(func, sentinel) calls func() until it returns that value.
StopIteration
The exception next() raises when an iterator has no items left. A for loop catches it and ends quietly, which is why a loop never shows it.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.