Advanced PythonAdvanced9 min55 / 63

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

Visual walkthrough1 / 6
1

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.

An iterable is the playlist. An iterator is the play button — it tracks where you are.
Think of it like

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").

Using iter() and next() manually on a list
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

A for loop is iter() + repeated next() + catching StopIteration
# 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 (usually self).
  • __next__(self) — returns the next value, or raises StopIteration when done.

Python calls these automatically. You never invoke __iter__ or __next__ by name — Python's iter() and next() do it for you.

A custom iterator that counts down — works with for, list(), sum(), and more
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)
Common mistake

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.

The same Countdown logic as a generator — much shorter, memory-friendly
def countdown(start):
    while start > 0:
        yield start   # pause, hand back the value
        start -= 1

for number in countdown(5):
    print(number)
Quick check

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.
Practice challenges
Test yourself · earn XP
0/4
Predict the output#1

What does this code print?

predict-output
colors = ["red", "green", "blue"]
it = iter(colors)
print(next(it))
print(next(it))
print(next(it, "none left"))
print(next(it, "none left"))
Fix the bug#2

This class is supposed to count up from 1 to the given stop value, but it has a bug. What is wrong?

fix-bug
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)
Fill in the blank#3

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)
Reorder the lines#4

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.

1
    except StopIteration:
2
it = iter(items)
3
while True:
4
        break
5
        val = next(it)
6
        print(val)
7
items = ["a", "b", "c"]
8
    try:
Your turn
Practice exercise

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:

solution.py · editable