Iterators: __iter__ & __next__
Reviewed & published by Brayan K
The iterator protocol is the simple contract behind every Python loop: an iterable returns an iterator from its __iter__ method, and that iterator hands back one value per call to __next__ until it raises StopIteration to signal the end.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Understanding this protocol demystifies the for-loop, generators, and half the standard library — and lets you build your own loopable objects.
What You'll Learn in This Lesson
- The difference between iterable and iterator
- Driving iteration by hand with iter() and next()
- How StopIteration ends a loop
- Writing a custom iterator class
- How a for-loop really works inside
- When to prefer a generator instead
1 iter(), next() & StopIteration
Call iter() on any iterable to get an iterator, then call next() to pull values one at a time. When the items run out, next() raises StopIteration:
names = ["Ada", "Ben", "Cleo"]
# A list is iterable; iter() gives us its iterator
it = iter(names)
print(type(it)) # <class 'list_iterator'>
# next() pulls one value at a time, remembering position
print(next(it)) # Ada
print(next(it)) # Ben
print(next(it)) # Cleo
# One more call and the iterator is exhausted
try:
next(it)
except StopIteration:
print("No more items!")
# next() can take a default to avoid the exception
print(next(it, "DONE")) # DONE# A for-loop is really iter() + next() + StopIteration handling.
# Here's the manual version that does the same work:
data = [10, 20, 30]
it = iter(data)
while True:
try:
value = next(it)
except StopIteration:
break
print("Got:", value)
print("Loop finished")
# Expected output:
# Got: 10
# Got: 20
# Got: 30
# Loop finished2 Writing a Custom Iterator Class
To make your own loopable object, implement two dunder methods: __iter__ returns the iterator (usually self), and __next__ returns the next value or raises StopIteration:
class CountDown:
def __init__(self, start):
self.current = start
def __iter__(self):
# Return the iterator object (here, the instance itself)
return self
def __next__(self):
if self.current <= 0:
raise StopIteration # the agreed end signal
self.current -= 1
return self.current + 1
# Now it works in a normal for-loop, no extra code needed
for n in CountDown(3):
print(n)
# 3
# 2
# 1
# It also works anywhere iteration is expected
print(list(CountDown(4))) # [4, 3, 2, 1]Because the class implements the protocol, every Python construct that loops — for, list(), sum(), comprehensions, unpacking — works on it automatically. That's the power of following the protocol.
3 Iterators vs Generators
A generator function with yield produces an iterator for free — Python writes __iter__ and __next__ behind the scenes. Compare the same countdown both ways:
# The generator version of the same CountDown — far shorter
def countdown(start):
while start > 0:
yield start
start -= 1
print(list(countdown(3))) # [3, 2, 1]
# A generator IS an iterator: iter() and next() work directly
g = countdown(2)
print(next(g)) # 2
print(next(g)) # 1
# It also raises StopIteration when done
try:
next(g)
except StopIteration:
print("exhausted") # exhausted# A reusable iterable: __iter__ returns a FRESH iterator each time
class Squares:
def __init__(self, n):
self.n = n
def __iter__(self):
# A generator gives us a brand-new iterator on every loop
for i in range(1, self.n + 1):
yield i * i
sq = Squares(4)
print(list(sq)) # first loop
print(list(sq)) # second loop works too — it's reusable!
# ✅ Expected output:
# [1, 4, 9, 16]
# [1, 4, 9, 16]🎯 Your Turn: Fill in the Blanks
Replace each ___ so the class is a valid iterator that yields 1, 2, 3 then stops.
# 🎯 YOUR TURN: complete the iterator protocol
class UpTo:
def __init__(self, limit):
self.limit = limit
self.value = 0
def __iter__(self):
return ___ # an iterator returns itself here
def __next__(self):
if self.value >= self.limit:
raise ___ # signal that iteration is over
self.value += 1
return self.value
print(list(UpTo(3)))
# Expected output:
# [1, 2, 3]Common Errors
❌ Forgetting to raise StopIteration
def __next__(self):
return self.value # never stops -> infinite loop✅ Raise StopIteration when done
def __next__(self):
if self.done:
raise StopIteration
...❌ __iter__ returning a non-iterator
def __iter__(self):
return [1, 2, 3] # a list isn't an iterator -> TypeError on use✅ Return self or iter(...)
def __iter__(self):
return iter([1, 2, 3]) # iter() gives a real iterator❌ Expecting an exhausted iterator to restart
it = iter([1, 2])
list(it) # [1, 2]
list(it) # [] — already exhausted!✅ Get a fresh iterator from the iterable
data = [1, 2]
list(iter(data)) # [1, 2]
list(iter(data)) # [1, 2] — fresh each time🎯 Mini Challenge: Fibonacci Iterator
Build an iterator that yields the first n Fibonacci numbers using the protocol — a classic that shows off lazy, stateful iteration.
# 🎯 Mini Challenge: a Fibonacci iterator class
class Fib:
def __init__(self, n):
self.n = n
self.count = 0
self.a, self.b = 0, 1
def __iter__(self):
return self
def __next__(self):
if self.count >= self.n:
raise StopIteration
self.count += 1
value = self.a
self.a, self.b = self.b, self.a + self.b
return value
print(list(Fib(10)))
# Expected output:
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]📚 Keep Learning
- Python tutorial: Iterators
- Python docs: Iterator Types (the protocol in detail)
- Python docs: the iter() and next() built-ins
Frequently Asked Questions
Lesson complete — the for-loop holds no secrets!
You can tell an iterable from an iterator, drive iteration with iter() and next(), handle StopIteration, write a custom iterator class with __iter__ and __next__, and you know when a generator is the simpler choice.
Practice quiz
What is the difference between an iterable and an iterator?
- They are identical
- An iterator is always a list
- An iterable produces an iterator; an iterator walks through items via __next__
- An iterable has no __iter__
Answer: An iterable produces an iterator; an iterator walks through items via __next__. An iterable returns an iterator from __iter__; the iterator yields items one at a time via __next__.
Which two dunder methods make up the iterator protocol?
- __iter__ and __next__
- __init__ and __call__
- __get__ and __set__
- __enter__ and __exit__
Answer: __iter__ and __next__. An iterator implements __iter__ (returns itself) and __next__ (returns the next value).
What does an iterator's __next__ do when there are no items left?
- Returns None
- Returns 0
- Loops forever
- Raises StopIteration
Answer: Raises StopIteration. __next__ raises StopIteration to signal the end of iteration.
How does a for-loop end when looping over an iterator?
- It counts the length first
- It catches StopIteration and stops
- It checks for None
- It runs a fixed number of times
Answer: It catches StopIteration and stops. The for-loop repeatedly calls next() and stops when StopIteration is raised.
Given an exhausted iterator it, what does next(it, 'DONE') return?
- 'DONE'
- StopIteration
- None
- An error
Answer: 'DONE'. next() with a default returns that default instead of raising StopIteration.
After it = iter([1, 2]); list(it), what does a second list(it) give?
- [1, 2]
- [2]
- []
- StopIteration
Answer: []. An iterator is one-shot; once exhausted it yields nothing, so the second list() is empty.
For the Fibonacci iterator in the lesson, what does list(Fib(10)) produce?
- [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
- [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
- [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
- [1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
Answer: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]. Starting from a=0, b=1, the first 10 Fibonacci numbers are [0, 1, 1, 2, 3, 5, 8, 13, 21, 34].
What should __iter__ return to satisfy the protocol?
- A list
- None
- A string
- Something with a __next__ method (often self)
Answer: Something with a __next__ method (often self). Returning a non-iterator (like a list) causes TypeError: iter() returned non-iterator; return self or iter(...).
Why is a generator function (using yield) often simpler than a full iterator class?
- It runs faster always
- Python writes __iter__ and __next__ for you
- It needs no StopIteration ever
- It cannot be exhausted
Answer: Python writes __iter__ and __next__ for you. A generator gives you the __iter__/__next__ machinery automatically.
What does list(CountDown(4)) produce for the lesson's CountDown iterator?
- [0, 1, 2, 3]
- [1, 2, 3, 4]
- [4, 3, 2, 1]
- [3, 2, 1, 0]
Answer: [4, 3, 2, 1]. CountDown(4) yields 4, 3, 2, 1 before raising StopIteration.
Continue this course
- Previous: Strings, Bytes & Unicode Encodings
- Next: Checkpoint: Data Structures — Combine Counter, heapq, deque and bisect in a build — then a quiz
- Quick reference: Python cheat sheet
Frequently asked questions
What is the difference between an iterable and an iterator?
An iterable is anything you can loop over — a list, string, dict, or set — and it knows how to produce a fresh iterator on demand via its __iter__ method. An iterator is the object that actually walks through the items one at a time, remembering its position, via its __next__ method. Every iterator is also an iterable, but not every iterable is an iterator.
What is StopIteration?
StopIteration is the exception an iterator raises from __next__ when there are no items left. It's the agreed-upon signal that iteration is finished. A for-loop catches StopIteration automatically and ends the loop, which is why you rarely see it directly — but if you call next() by hand, you'll hit it when the iterator is exhausted.
How does a for-loop actually work under the hood?
A for-loop first calls iter() on the iterable to get an iterator, then repeatedly calls next() on that iterator, binding each returned value to the loop variable. When next() raises StopIteration, the loop stops. So 'for x in items' is really a tidy wrapper around iter() and next() plus exception handling.
When should I write a class versus use a generator?
Generators (functions that yield) are far simpler and cover most cases, because Python writes the __iter__ and __next__ machinery for you. Write a full iterator class when you need extra state, methods, or behaviour beyond yielding values — for example an object that's both iterable and has other useful methods. For pure iteration, prefer a generator.