Python Iterators: What a for Loop Actually Does
What iter() and next() do behind every for loop, and how to write your own iterator. After this, you understand why some objects can only be looped over once.
Course content
Sign in to save your progress.
-
Functions That Wrap Functions
-
Python Closures: Functions That Remember
-
Python Decorators in Real Code: Timing, Retry and Logging
-
-
-
Data That Doesn't Fit in Memory
-
Python Iterators: What a for Loop Actually Does
-
Python Generators and yield: One Piece at a Time
-
Python Context Managers: Clean Up Even When Things Fail
-
-
-
Catch Bugs Before You Run the Code
-
Python Type Hints Beyond the Basics: Optional, Union, Generics
-
Checking Types With mypy in a Real Project
-
-
-
Doing More Than One Thing at a Time
-
Python Concurrency Basics: Threads, Processes or asyncio?
-
Python asyncio: Wait for Many Things Without Waiting Longer
-
-
-
Ship It, Then Make It Fast
-
Packaging a Python Project With pyproject.toml
-
Profiling Python Code: Measure Before You Optimize
-
-
- Final Check: Python Advanced
Every for loop in Python does the same two things behind the scenes: it asks the object for an iterator with iter(), then calls next() on it until it runs out. Once you see that, a lot of odd behaviour makes sense, like why a file or a zip can only be looped over once.
Skip ahead: If you know the difference between an iterable and an iterator, what StopIteration is for, and how to write __iter__ and __next__, continue to "Python Generators and yield: One Piece at a Time."
What a for loop actually does
colors = ["red", "green", "blue"]
it = iter(colors)
print(next(it))
print(next(it))
print(next(it))
try:
next(it)
except StopIteration:
print("StopIteration: the loop ends here")
red
green
blue
StopIteration: the loop ends here
That is all for color in colors: is. It calls iter() once, then next() again and again, and stops quietly when it gets StopIteration.
Iterable vs iterator
Two words that sound the same and mean different things:
- An iterable is something you can loop over: a list, a string, a dictionary, a file. It can give you a new iterator every time you ask.
- An iterator is the thing doing the looping. It remembers its position and only goes forward. When it is used up, it stays used up.
numbers = [1, 2, 3]
print(list(numbers), list(numbers)) # a list is iterable: loop as often as you like
pairs = zip(["a", "b"], [1, 2]) # zip returns an iterator
print(list(pairs), list(pairs)) # the second time, it is empty
print(iter(pairs) is pairs) # an iterator's iter() returns itself
[1, 2, 3] [1, 2, 3]
[('a', 1), ('b', 2)] []
True
That empty second list is a real bug pattern. You pass a zip, map, filter, file or generator to a function that loops over it twice, for example once to count and once to process. The second loop gets nothing, and there is no error.
⚠ Warning: If a function needs to loop over its input more than once, either convert it with list() at the start (only if it fits in memory) or design the function to need one pass. Don't assume the caller passed a list. A totals function that works in your test with a list and returns 0 in production with a generator is exactly this bug.
Writing your own iterator
A class becomes an iterator by having two methods: __iter__ returns the iterator (itself), and __next__ returns the next value or raises StopIteration. Here is one that hands out records in pages, like an API that returns results 100 at a time:
class Pages:
def __init__(self, items, size):
self.items = items
self.size = size
self.position = 0
def __iter__(self):
return self
def __next__(self):
if self.position >= len(self.items):
raise StopIteration
page = self.items[self.position:self.position + self.size]
self.position += self.size
return page
for page in Pages(list(range(1, 8)), size=3):
print(page)
[1, 2, 3]
[4, 5, 6]
[7]
It works, but notice two things. First, it is a lot of code for a simple idea. Second, it is single-use: loop over the same Pages object again and you get nothing. The next lesson writes the same thing with a generator in five lines.
Making a reusable iterable
If you want an object that can be looped over many times, make __iter__ return a new iterator each time instead of self:
class Catalog:
def __init__(self, products):
self.products = products
def __iter__(self):
return iter(self.products) # a fresh iterator every time
catalog = Catalog(["Keyboard", "Mouse"])
print(list(catalog))
print(list(catalog))
print("Mouse" in catalog)
['Keyboard', 'Mouse']
['Keyboard', 'Mouse']
True
As a bonus, in, list(), sorted(), sum() and unpacking all work, because they all use the same iter()/next() protocol.
Useful tools in itertools
The itertools module works on iterators without building lists. Three you will use:
from itertools import islice, chain, count
print(list(islice(count(start=1000), 3))) # first 3 of an endless counter
print(list(chain([1, 2], (3, 4), "ab"))) # several iterables as one
log_lines = iter(["header", "row 1", "row 2", "row 3"])
next(log_lines) # skip the header
print(list(islice(log_lines, 2))) # take only the next two
[1000, 1001, 1002]
[1, 2, 3, 4, 'a', 'b']
['row 1', 'row 2']
count() never ends. That is fine, because islice only asks for three values. An iterator only does work when someone calls next(). This laziness is what makes processing huge files possible, and it is the core of the next lesson.
Common mistakes
- Looping over an iterator twice. Covered above. The second loop is silently empty.
- Calling
len()on an iterator.len(zip(a, b))is aTypeError. Iterators don't know how many items are left until they get there. - Changing a list while looping over it. The iterator tracks a position, and removing items shifts everything after it, so items get skipped. Loop over a copy, or build a new list.
💡 Tip: next(iterator, default) returns the default instead of raising StopIteration when nothing is left. next((o for o in orders if o["id"] == 7), None) is a clean way to find the first match or get None.
What happens if you skip this step
You keep running into "it worked with a list, but not with this" bugs and treat each one as a separate mystery. They are all the same thing: an iterator only goes forward, once. Knowing that turns a confusing class of bugs into an obvious one.
A function counts the rows of its input, then sums them. You pass it an open file. What does the sum come out as, and why?
About
What iter() and next() do behind every for loop, and how to write your own iterator. After this, you understand why some objects can only be looped over once.
- Duration
- 5 min
- Last update
- 09/11/2026
Comments (0)
Sign in to commentNo comments yet.