Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 3 of 10

You've used for since Module 2 without knowing how it works. The answer is the iterator protocol — two methods that any object can implement to become loopable.

Iterable versus iterator

  • An iterable can produce an iterator. It has __iter__(). Lists, strings, dicts, files.
  • An iterator produces values one at a time. It has __next__() (and an __iter__() that returns itself).
numbers = [1, 2, 3]

it = iter(numbers)
print(type(it).__name__)

print(next(it))
print(next(it))
print(next(it))

A fourth next() raises StopIteration.

What for really does

This loop:

for n in [1, 2, 3]:
    print(n)

is shorthand for exactly this:

numbers = [1, 2, 3]
it = iter(numbers)

while True:
    try:
        n = next(it)
    except StopIteration:
        break
    print(n)

for calls iter(), then next() repeatedly, and stops when StopIteration is raised. That's the whole mechanism — and it's why generators from lesson 9.2 work in for loops for free: a generator is an iterator.

Iterable but not an iterator

A list can be looped many times because each for gets a fresh iterator:

numbers = [1, 2, 3]

print(list(iter(numbers)) == list(iter(numbers)))

it = iter(numbers)
print(list(it))
print(list(it))

The list is reusable; a single iterator taken from it is not. That's the same one-shot behaviour you saw with generators.

Writing your own iterator

Implement both methods and your class works in a for loop:

class Countdown:
    """Counts down from start to 1."""

    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

for n in Countdown(3):
    print(n)

Note this class is single-use — self.current is consumed. Returning a fresh iterator from __iter__ is what makes something reusable.

Usually, just write a generator

The class above is nine lines. A generator does the same in three:

def countdown(start):
    while start > 0:
        yield start
        start -= 1

print(list(countdown(3)))

Generators are iterators — Python writes the __iter__ and __next__ for you. Write the class only when you need extra state or methods alongside the iteration.

The simplest option of all is often __getitem__ from lesson 5.10, which makes an object iterable without either protocol method.

Useful iterator tools

itertools is built on this protocol:

import itertools

print(list(itertools.islice(itertools.count(10), 4)))
print(list(itertools.chain([1, 2], [3, 4])))
print([list(g) for _, g in itertools.groupby([1, 1, 2, 2, 3])])
print(list(zip([1, 2, 3], "abc")))

itertools.count() is infinite — islice is what makes taking the first four safe. Infinite iterators are practical precisely because nothing is computed until requested.

Try it yourself

Checkpoint

  • An iterable has __iter__; an iterator has __next__ and raises StopIteration when done.
  • for is iter() plus repeated next() with StopIteration as the exit.
  • Lists are reusable iterables; an iterator taken from one is single-use.
  • Generators are iterators — prefer them over hand-written iterator classes.
  • itertools composes iterators, including infinite ones.

Questions about this lesson

Log in to ask a question.