Python Iterable vs Iterator: iter, next and StopIteration
An iterable gives an iterator via iter(); the iterator returns values from next() until StopIteration. How for loops use it and how to write an iterator class.
- Course: Python study plan
- Module: Iterators, generators and itertools
- Kind: Lesson
- Reading time: 13 min
- Runtime: CPython 3.11
What is the difference between an iterable and an iterator in Python?
An iterable is any object iter() accepts, because it has __iter__ (or __getitem__): a list, string, dict, range or file. An iterator is the object that produces the values: its __next__ returns the next value or raises StopIteration, and its __iter__ returns itself. A list can be looped over any number of times; an iterator is one-shot.
Lesson
Every for loop, comprehension, sum, list() and zip in Python runs on one small protocol: ask an object for an iterator with iter(), call next() on it until it raises StopIteration. Understanding that protocol explains why a file can be looped over only once, why zip and enumerate are lazy, why range can be iterated twice but map(...) cannot, and how to write a class that for understands. This lesson defines iterable and iterator precisely, desugars the for loop, shows the built-in iterators and what exhaustion means, writes an iterator class by hand, and covers the two-argument iter(callable, sentinel) form.
Iterable versus iterator
- An iterable is anything
iter()accepts: it has__iter__(returning an iterator) or__getitem__with integer indexes. Lists, strings, dicts, sets, ranges, files, generators. - An iterator is the object that produces the values: it has
__next__(returning the next value or raisingStopIteration) and__iter__returning itself, so that an iterator is also an iterable.
xs = [1, 2, 3]
it = iter(xs) # a list_iterator
next(it) # 1
next(it) # 2
next(it) # 3
next(it) # StopIteration
next(it, "done") # 'done' — a default instead of the exception
A list is iterable but not an iterator: next(xs) is TypeError: 'list' object is not an iterator. Calling iter(xs) again gives a fresh iterator starting at the beginning; the list can be looped over any number of times. The iterator itself is one-shot: once it has raised StopIteration, it raises it forever.
The for loop, desugared
for x in xs:
body(x)
# is exactly:
_it = iter(xs)
while True:
try:
x = next(_it)
except StopIteration:
break
body(x)
So for calls iter() once and next() per iteration, and stops on StopIteration. Every other consumer — list(xs), sum(xs), "".join(xs), max(xs), x in xs without __contains__, unpacking a, b = xs — does the same. That is why the protocol is the only thing a class needs to implement to work everywhere.
Lazy built-ins
zip, enumerate, map, filter, reversed, range iterators, dict views' iterators, file objects and generators all produce values on demand and are iterators (or produce one), not lists:
pairs = zip([1, 2], "ab")
list(pairs) # [(1, 'a'), (2, 'b')]
list(pairs) # [] — exhausted; zip returned a one-shot iterator
r = range(3)
list(r), list(r) # [0, 1, 2] twice — range is an iterable, iter(r) makes a fresh iterator each time
with open(path) as f:
lines = list(f) # a file is its own iterator: after this it is at the end
more = list(f) # []
The practical rule: if you need to go over the values twice, materialise them (list(...)) or make sure you have an iterable rather than an iterator. itertools.tee splits an iterator into several (Module 11 lesson 4), at the cost of buffering.
Writing an iterator class
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self # an iterator returns itself
def __next__(self):
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1
list(Countdown(3)) # [3, 2, 1]
c = Countdown(2)
list(c), list(c) # [2, 1], [] — one-shot, like every iterator
The state lives in the object; __next__ advances it and raises when done. To make a re-iterable container, give the container an __iter__ that returns a new iterator each time — a separate class, or (next lesson) a generator, which is what almost every real __iter__ is:
class Playlist:
def __init__(self, tracks):
self._tracks = list(tracks)
def __iter__(self):
return iter(self._tracks) # a fresh iterator per loop
iter with a sentinel
iter(callable, sentinel) calls callable() repeatedly until it returns sentinel:
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
process(chunk)
for line in iter(input, "END"): # read until a line equals "END"
handle(line)
The second form is the cleanest "read until sentinel" loop in the language — no while True, no break.
The protocol elsewhere
in falls back on iteration when there is no __contains__; reversed needs __reversed__ or the sequence protocol; len is not part of iteration — an iterator has no length, which is why len(zip(...)) is a TypeError and why sum(1 for _ in it) is how you count one. Unpacking a, *rest = it consumes the iterator entirely. bool(it) is always True for an iterator — emptiness cannot be tested without consuming.
Pitfalls
- Iterating an iterator twice and getting nothing the second time.
len()on an iterator.next()on an iterable (a list) instead ofiter(list).- A
__next__that never raisesStopIteration— every loop over it is infinite. - Testing
if it:for emptiness. - Returning a list from
__iter__(must be an iterator;iter(list)fixes it).
Key takeaways
- Iterable: has
__iter__(or__getitem__); iterator: has__next__and__iter__returning itself;forcallsiter()once andnext()untilStopIteration. - Iterators are one-shot; containers give a fresh iterator each time;
zip,map, files and generators are one-shot iterators. - Write
__next__to advance state and raiseStopIterationwhen done; give a container an__iter__that returns a new iterator. next(it, default)avoids the exception;iter(callable, sentinel)loops until a value.- Iterators have no
len, no emptiness test and cannot be rewound — materialise when you need those.
Common questions
How does a for loop work in Python?
A for loop calls iter() on the object once, then calls next() on the resulting iterator for each pass and stops when it raises StopIteration. list(), sum(), max(), join and unpacking consume objects the same way, so implementing the protocol makes a class work with all of them.
Why does zip or map return nothing the second time?
zip, map, filter, enumerate, file objects and generators are iterators, and an iterator is one-shot: once it has raised StopIteration, it stays exhausted. Lists and range are iterables that hand out a fresh iterator each time. Materialise with list(...) when you need two passes.
How do you write an iterator class in Python?
Give the class an __iter__ that returns self and a __next__ that advances the stored state and returns the next value, raising StopIteration when there are no more. For a container that can be looped over repeatedly, make __iter__ return a new iterator each time, usually by writing it as a generator.
What does "TypeError: 'list' object is not an iterator" mean?
It means next() was called on a list. A list is iterable but not an iterator, so call iter(xs) first and call next() on the result. next(it, default) returns the default instead of raising StopIteration when the iterator is exhausted.
What does iter() with two arguments do?
iter(callable, sentinel) calls callable() repeatedly and yields each result until one equals sentinel. iter(lambda: f.read(4096), b"") reads a binary file in chunks until the empty bytes at the end: the cleanest read-until-sentinel loop, with no while True and no break.
Exercises
A range of your own
Implement Span(start, stop, step) as an iterator class with __iter__ returning self and __next__ producing start, start + step, … while below stop (step is positive), then raising StopIteration. Read start stop step, print the values from a first pass, then try a second pass over the same object and print exhausted if it yields nothing. Finally print the values from Span(...) consumed with next and a default, three times.
Input: start stop step. Output: first: <values> (or first: (empty)), second: <values> or second: exhausted, then three lines from next(it, "done").
1 8 3
prints
first: 1 4 7
second: exhausted
1
4
7Until the sentinel
Use the two-argument form iter(input, "END") to read integer lines until a line equal to END, and print their sum and count. Then read the rest of the input with a plain for line in sys.stdin loop and print how many lines follow the sentinel.
Input: integer lines, END, then any further lines. Output: sum <s> count <n>, then after <k>.
3
4
END
x
y
prints
sum 7 count 2
after 2In this module: Iterators, generators and itertools
- The iteration protocol — iter, next and StopIteration (this lesson)
- Generators — functions that yield
- Generator expressions — lazy comprehensions
- itertools — the iterator toolkit
- functools and operator — the function toolkit
- Lazy pipelines — a worked log-processing example
- Checkpoint — Iterators, generators and itertools
← Checkpoint — Errors and exceptions · Generators — functions that yield →