Skip to main content

Iterables and iterators

What a for loop really does: it asks for an iterator with iter, calls next until StopIteration, and stops. Write the loop by hand, build a Countdown iterator class, and see why a zip or a file can be looped over only once.
P01.10.2L0 Foundation25 minIn review

Before this lesson

P01.10.1 Comprehensions P01.9.3 Special methods: objects that act like built-ins

After this lesson you can

  • Explain what a for loop does with iter, next and StopIteration, and rewrite a loop by hand with while, try and break.
  • Tell an iterable from an iterator: a list hands out a fresh iterator each time, an iterator is used up after one pass.
  • Write an iterator class with iter and next, and check it against range(3, 0, -1).
  • Find and repair the bug of looping twice over a zip or a file.

In review. The maths and code on this page are checked by automated tests on every change, but an independent reader has not signed it off yet. Spot a mistake or a confusing step? Tell us.

The question

You loop over the pairs of zip(hours, scores) to add up the squared errors, then loop over the same pairs again to count them. The first loop works. The second one finds nothing at all, and the mean squared error divides by zero. A list never does this. What is different about a zip?

From the last lesson

1. What are the three parts of [s for s in scores if s >= 50]?

Show answer

The output expression s, the for clause for s in scores, and the if clause if s >= 50.

2. What is the difference between {band_of(s) for s in scores} and [band_of(s) for s in scores]?

Show answer

The first is a set, with each band once (three items, no positions); the second is a list with one band per score (five items, in order).

Intuition

A for loop does not walk over a list by itself. It asks the list for a helper, an object whose only job is to hand over the items one at a time and to remember where it has got to. That helper is an iterator. Think of it as a bookmark in the list.

The list scores drawn as five numbered slots, 55, 65, 80, 40, 60. Below it the name it points to an iterator object, of type list_iterator, which holds next position 3 and an arrow up to slot 3. On the right, the three calls made so far: next(it) gave 55, next(it) gave 65, next(it) gave 80, the last one filled yellow. Below them the calls still to come: 40, then 60, then a sixth call that raises StopIteration because no items are left.
P01.10.2-F1

An iterator is a bookmark. Each call to next hands over the item at the bookmark and moves it on by one; after the last item, it raises StopIteration.

Two things are needed, and they have two names. The list is iterable: it can hand out bookmarks. The bookmark is the iterator: it hands out items. A list can make as many bookmarks as you like, each starting at position 0. A bookmark only moves forward.

Build it up

Step 1. Ask for an iterator. The built-in iter takes an iterable and returns a new iterator.

scores = [55, 65, 80, 40, 60]
hours = [2, 4, 5, 1, 3]

it = iter(scores)
print(type(it).__name__)
list_iterator

Step 2. Take items with next. Each call to next(it) hands over one item and moves the bookmark on.

print(next(it))
print(next(it))
print(next(it), next(it), next(it))
55
65
80 40 60

Step 3. Run off the end. All five items have been handed over. One more call raises StopIteration, the exception that means “no items are left”. The try and except of P01.8.2 catch it.

try:
    next(it)
except StopIteration:
    print('StopIteration: no items are left')
StopIteration: no items are left

Step 4. Put the three together. That is all a for loop does: iter once, then next again and again until StopIteration, which it catches quietly and takes as the signal to stop.

A flow chart of for s in scores: print(s). A rounded Start box leads to the action it = iter(scores), then to a diamond that asks: does next(it) raise StopIteration? Its False exit leads down to the action s = the item it gave, then to print(s), and a green arrow loops back up to the diamond. Its True exit leads right to a rounded box: the loop ends. A note beside the loop says this runs 5 times, once per score; a note at the True exit says the sixth call raises.
P01.10.2-F2

What for s in scores: print(s) does. The loop asks for one iterator, then calls next until it raises StopIteration.

Here is the same loop written out by hand, with while True and break from P01.3.4.

it = iter(scores)
while True:
    try:
        s = next(it)
    except StopIteration:
        break
    print(s)
55
65
80
40
60

It prints the five scores, exactly as for s in scores: print(s) does.

The formal statement

NoteIterables, iterators and the for statement
  • An iterator is an object with a method __next__, which next(it) calls. Each call returns the next item, or raises StopIteration when there are none left. An iterator also has __iter__, which returns the iterator itself.
  • An iterable is an object that iter(x) can turn into an iterator, by calling its method __iter__. Lists, tuples, strings, dictionaries, sets, ranges and files are iterable.
  • The statement for name in iterable: runs it = iter(iterable) once, then binds name to next(it) and runs the body, again and again, until next(it) raises StopIteration.

The Python Tutorial (section 9.8) says it in one breath: “the for statement calls iter() on the container object”, and when there are no more elements, __next__() “raises a StopIteration exception which tells the for loop to terminate”.

These are special methods with fixed names, like the __eq__ and __add__ of P01.9.3: you write next(it) and Python calls it.__next__().

Fresh or used up. Every iterator is iterable, because iter(it) returns it itself. The difference between them is what happens on a second pass. The Python glossary says that a list “produces a fresh new iterator each time you pass it to the iter() function or use it in a for loop”, while an iterator passed to iter again just returns “the same exhausted iterator object”, which makes it look like an empty container.

Two panels. Left, titled a list hands out fresh iterators: the list scores with two separate iterator objects below it, a at position 3 and b, drawn in the second colour, at position 1, each with its own arrow up to its slot. Right, titled an iterator is its own iterator: pairs = zip(hours, scores) is one zip object with one position, 5 of 5, used up; iter(pairs) gives back the same object, drawn as an arrow that loops back to it, so a second loop starts where the first one finished and finds nothing.
P01.10.2-F4

A list makes a new bookmark for each loop. A zip is a bookmark already, so a second loop gets the same one, at the end.

zip, enumerate, files and the generators of the next lesson are all iterators. Each can be looped over once.

Worked example

An iterator of your own needs a class with both special methods. Countdown(3) should hand over 3, 2 and 1, the same numbers as range(3, 0, -1), which counts down by steps of -1 and stops before 0.

class Countdown:
    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

Trace next on Countdown(3) by hand. The object holds one attribute, current, which starts at 3.

A trace table for c = Countdown(3) with the columns call, current before, returns and current after. Call 1: current is 3, it returns 3, current becomes 2. Call 2: 2, returns 2, becomes 1. Call 3: 1, returns 1, becomes 0. Call 4, shaded: current is 0, so it raises StopIteration and current stays 0. Below: list(Countdown(3)) is [3, 2, 1], the same as list(range(3, 0, -1)).
P01.10.2-F3

The trace of Countdown(3). Each call hands over the current value and lowers it by one; at 0 it raises StopIteration.

  • Call 1. current is 3, not 0, so value is 3, current becomes 2, and 3 is returned.
  • Calls 2 and 3. They return 2 and 1, and leave current at 1 and then 0.
  • Call 4. current is 0, so __next__ raises StopIteration. A for loop would stop here.

So a loop over Countdown(3) sees 3, 2 and 1, and so does list, which loops in the same way.

Code

The class against range, and the proof that the object is its own iterator.

print(list(Countdown(3)))
print(list(range(3, 0, -1)))

c = Countdown(3)
print(iter(c) is c)
print(list(c), list(c))
[3, 2, 1]
[3, 2, 1]
True
[3, 2, 1] []

The second list(c) is empty: the first one used the iterator up. A range, like a list, hands out a fresh iterator every time, so list(r) twice gives the numbers twice.

Now the bug of the question. Both loops below walk over the same zip.

def predict(hours):
    return 9 * hours + 33


pairs = zip(hours, scores)
total = 0
for h, s in pairs:
    total += (s - predict(h)) ** 2
count = 0
for h, s in pairs:
    count += 1
print(total, count)
40 0

The squared errors add up to 40, the right total. The count is 0, because the first loop left the zip at its end. Dividing would raise ZeroDivisionError. The repair is to make a list once, which can be looped over as often as you like.

pairs = list(zip(hours, scores))
total = 0
for h, s in pairs:
    total += (s - predict(h)) ** 2
count = 0
for h, s in pairs:
    count += 1
print(total, count, total / count)
40 5 8.0

A file behaves the same way: a file object is its own iterator, and its bookmark is the position in the file. Here is study.csv, written to a temporary folder as in P01.7.3, and looped over twice.

import pathlib
import tempfile

lines = ['name,hours,score', 'Ama,2,55', 'Ben,4,65', 'Chen,5,80', 'Dina,1,40', 'Eli,3,60']

with tempfile.TemporaryDirectory() as folder:
    path = pathlib.Path(folder) / 'study.csv'
    path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
    with open(path, encoding='utf-8') as f:
        first = 0
        for line in f:
            first += 1
        second = 0
        for line in f:
            second += 1
print(first, second)
6 0

Six lines the first time (the header and five students), none the second.

Explore

This module’s interactive, the lazy and eager pipeline, lives in P01.10.3. In its lazy mode, each press of the step button reads one number and pulls it as far through the stages as it gets. Watch how next on the last stage asks the stage before it for an item, which asks the stage before that, all the way back to the source.

Pitfalls

Wrong, with a cross: pairs = zip(hours, scores), then two loops over pairs. The first loop adds the squared errors and gets 40; the second loop counts the pairs and gets 0, because the zip is used up, so 40 / 0 raises ZeroDivisionError. Right, with a tick: pairs = list(zip(hours, scores)), a list of five pairs drawn as slots. The first loop gets 40, the second counts 5, and the mean squared error is 40 / 5 = 8.0.
P01.10.2-F5

Wrong beside right. A zip is used up by the first loop; a list made from it can be looped over twice.

“I can loop over anything as often as I like.”

Only over an iterable that hands out fresh iterators, such as a list, a tuple, a string, a dictionary or a range. A zip, an enumerate, a file object or a generator is an iterator: the second loop starts where the first one stopped, at the end, and sees nothing. No error is raised, so the bug shows up later, as a count of 0 or an empty result. When you need two passes, make a list once.

“StopIteration is an error, so my loop crashed.”

Inside a for loop, StopIteration is the normal way to end, and the loop catches it for you. It only shows up as an error when you call next yourself after the items have run out, and then a try is the way to handle it, as in step 3.

“print(zip(hours, scores)) should show me the pairs.”

It shows something like <zip object at 0x10a2b3c40>, a description of the iterator with a memory address that changes from run to run. The pairs are not made until something asks for them. Write list(zip(hours, scores)) to see them, and remember that this uses the zip up.

Check yourself

Recall

1. Which two built-in functions does a for loop use, and which exception ends it?

Show answer

iter, once, to get an iterator, and next, once per item. StopIteration ends it.

2. What is the difference between an iterable and an iterator?

Show answer

An iterable can hand out iterators (iter(x)); an iterator hands out items (next(it)) and remembers where it has got to. Every iterator is iterable, because iter(it) returns it itself.

3. Why does a second loop over a list see every item again?

Show answer

Because each loop calls iter on the list, and a list hands out a fresh iterator that starts at position 0 every time.

4. Which two methods must an iterator class define, and what does each one return?

Show answer

__iter__, which returns the object itself, and __next__, which returns the next item or raises StopIteration.

5. After c = Countdown(3), what are list(c) and then list(c) again?

Show answer

[3, 2, 1] and then []: the first call used the iterator up.

Exercises

1. ★ Predict the output, then explain it.

e = enumerate(['Ama', 'Ben', 'Chen'])
print(next(e))
print(list(e))
Show solution

(0, 'Ama'), then [(1, 'Ben'), (2, 'Chen')]. An enumerate is an iterator: the first next takes the first pair, and list takes the pairs that are left, starting from where the bookmark is.

2. ★★ Rewrite this loop with iter, next, while True, try and break, and say what it prints.

total = 0
for h in hours:
    total += h
print(total)
Show solution
total = 0
it = iter(hours)
while True:
    try:
        h = next(it)
    except StopIteration:
        break
    total += h
print(total)

Both print 15, the sum of 2, 4, 5, 1 and 3.

3. ★★★ Write an iterator class Every(items, step) that hands over items[0], items[step], items[2 * step] and so on, and check it on the scores with step 2 against the slice scores[::2], whose step works as in P01.7.1.

Show solution
class Every:
    def __init__(self, items, step):
        self.items = items
        self.step = step
        self.position = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.position >= len(self.items):
            raise StopIteration
        value = self.items[self.position]
        self.position += self.step
        return value

With step 2 the positions are 0, 2 and 4, then 6 is past the end. So list(Every(scores, 2)) is [55, 80, 60], the same as scores[::2].

Summary card

A for loop gets an iterator with iter and calls next until StopIteration.

The anchor figure again: the list of scores, an iterator object it with its next position as a bookmark, and the calls next(it) handing over 55, 65 and 80, with StopIteration after the last item.
P01.10.2-F1

An iterator is a bookmark.

  • Iterable: iter(x) gives an iterator. A list gives a fresh one each time.
  • Iterator: next(it) gives the next item or raises StopIteration; iter(it) is it.
  • Your own: a class with __iter__ returning self and __next__.
  • Used up: a zip, an enumerate, a file or a generator can be looped over once. Make a list for two passes.

Sources and next steps

Sources. Each was opened and checked against this lesson on 12 November 2026.

  • Python Software Foundation, The Python Tutorial, section 9.8, Iterators: the for statement calls iter(), and __next__() raises StopIteration to end the loop; the class Reverse, whose __iter__ returns self, is the model for Countdown.
  • Python Software Foundation, Glossary, entries “iterable” and “iterator”: a container “produces a fresh new iterator each time”, while an iterator, once exhausted, makes a second pass look “like an empty container”.
  • Python Software Foundation, Functional Programming HOWTO, section “Iterators”: “you can only go forward in an iterator; there’s no way to get the previous element, reset the iterator, or make a copy of it”.
  • Python Software Foundation, Built-in Functions: iter, next (which raises StopIteration when no default is given), enumerate, and zip, which “is lazy: The elements won’t be processed until the iterable is iterated on”.

Next. P01.10.3 Generators writes Countdown again in four lines, as a function that pauses.

This lesson unlocks

P01.10.3 Generators

Your progress is saved in this browser only. No account, no tracking.
Back to top