Iterators
Discover what actually happens inside a for loop, and learn to build your own iterable objects using __iter__ and __next__.
You have used for loops to walk through lists, strings, and ranges. But have you ever wondered what Python is actually doing under the hood? The answer involves two concepts: iterables and iterators.
- An iterable is anything you can loop over — a list, a string, a range.
- An iterator is the object that does the looping — it remembers your position and hands you the next item on demand.
Think of a book (iterable) and a bookmark (iterator). The book holds all the pages. The bookmark tracks which page you are on and knows which comes next.
See it in action
— step through the idea, then dive into the details below.What Lives Inside a for Loop?
Every time you write for x in something, Python is secretly using an iterator behind the scenes. Understanding this unlocks a whole new level of Python.
Playlist vs Play Button
A music playlist is an iterable — it contains songs but does not play anything by itself. Hitting play creates an iterator that starts at track 1, plays it, then moves to track 2. You can start the playlist from the top as many times as you like, each time getting a fresh iterator.
#iter() and next() — The Core Tools
Python exposes two built-in functions for working with iterators directly.
iter(obj)— asks an iterable for its iterator.next(iterator)— asks the iterator for the next value.
When no more items remain, next() raises StopIteration — Python's signal that the sequence is finished. You can also pass a default value as a second argument so next() returns that instead of raising: next(it, "done").
numbers = [10, 20, 30]
it = iter(numbers) # get an iterator from the list
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
print(next(it, "no more")) # safe default — no crash#What a for Loop Really Does
# Every for loop secretly does this:
my_list = [1, 2, 3]
it = iter(my_list) # step 1: get an iterator
while True:
try:
x = next(it) # step 2: fetch next item
print(x)
except StopIteration:
break # step 3: stop silently when done#Building Your Own Iterator Class
You can make any class work with for loops by implementing two special methods:
__iter__(self)— returns the iterator object (usuallyself).__next__(self)— returns the next value, or raisesStopIterationwhen done.
Python calls these automatically. You never invoke __iter__ or __next__ by name — Python's iter() and next() do it for you.
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self # this object is its own iterator
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for number in Countdown(5):
print(number)Iterators are one-shot by default
An iterator tracks its own position and is exhausted after one full loop. Looping over it again produces nothing:
```python c = Countdown(3) for n in c: print(n) # prints 3, 2, 1
for n in c: print(n) # prints NOTHING — already exhausted! ```
To iterate again, create a new instance. Lists sidestep this because iter(my_list) always produces a brand-new iterator.
Generators — Iterators the Easy Way
Writing __iter__ and __next__ by hand is powerful but verbose. Python's shortcut is a generator function — any function that uses the yield keyword. Python automatically turns it into a full iterator.
When Python hits yield, the function freezes in place, saving all local variables. The next time next() is called, the function resumes right after the yield. Generators also compute values one at a time, so a generator over a million items uses almost no memory — unlike a list that stores them all at once.
def countdown(start):
while start > 0:
yield start # pause, hand back the value
start -= 1
for number in countdown(5):
print(number)What does Python do when a for loop's iterator raises StopIteration?
Key takeaways
- An **iterable** is anything you can loop over; an **iterator** is the object that tracks position and delivers items one at a time.
- `iter(obj)` gets an iterator from an iterable; `next(it)` advances it; `StopIteration` signals the end.
- Every `for` loop secretly calls `iter()` once, then `next()` repeatedly until `StopIteration` is raised.
- Implement `__iter__` and `__next__` on a class to make it work with `for` loops and every other Python iteration tool.
- Generator functions using `yield` are the easiest way to create iterators — and they compute values lazily, saving memory.
What does this code print?
colors = ["red", "green", "blue"]
it = iter(colors)
print(next(it))
print(next(it))
print(next(it, "none left"))
print(next(it, "none left"))This class is supposed to count up from 1 to the given stop value, but it has a bug. What is wrong?
class Counter:
def __init__(self, stop):
self.current = 1
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.current > self.stop:
raise StopIteration
self.current += 1
return self.current
for n in Counter(3):
print(n)Complete the generator function so it yields each number from 1 up to and including n.
def count_up(n): i = 1 while i <= n: i i += 1 for num in count_up(3): print(num)
Put these lines in the right order so that the for loop manually mimics what Python does internally — getting an iterator, fetching items, and stopping cleanly.
except StopIteration:
it = iter(items)
while True:
break
val = next(it)
print(val)
items = ["a", "b", "c"]
try:
Write a generator function called squares(n) that yields the square of every integer from 1 up to and including n. Then use a for loop to print each result. For example, squares(5) should produce 1, 4, 9, 16, 25.
Try it live — edit the code and hit Run to execute real Python: