Iterators
Learn what makes an object iterable, how the iterator protocol works under the hood, and how to build your own custom iterator class.
Introduction
Every time you write a for loop over a list, a string, or a dictionary, Python is quietly running a well-defined protocol behind the scenes. That protocol is what makes iteration work, and it is called the iterator protocol.
Understanding iterators is one of those topics that makes a lot of "magic" in Python suddenly make sense — why some objects can be looped over, why you sometimes see a StopIteration error, and how tools like generators (covered in the next lesson) are able to produce values one at a time.
- The difference between an iterable and an iterator.
- The __iter__ and __next__ methods that make up the iterator protocol.
- How to manually pull values using iter() and next().
- What StopIteration means and when it is raised.
- How to build your own custom iterator class.
- How a for loop uses this protocol internally.
Iterable vs Iterator
These two words sound similar but mean different things. An iterable is any object you can loop over — like a list, tuple, string, or dictionary. An iterator is the object that actually does the looping, keeping track of where it currently is.
In short: an iterable knows how to produce an iterator, and an iterator knows how to produce the next value, one at a time, until there are none left.
Iterable
An object that can be iterated over. It has an __iter__() method that returns an iterator. Examples: list, tuple, str, dict, set.
Iterator
An object that produces values one at a time via __next__(). It has both __iter__() (returns itself) and __next__().
numbers = [1, 2, 3]
print(hasattr(numbers, '__iter__')) # True -> a list is iterable
print(hasattr(numbers, '__next__')) # False -> a list is NOT an iterator
iterator = iter(numbers)
print(hasattr(iterator, '__next__')) # True -> now it is an iteratorTrue
False
True- An iterable is like a book — it contains content you can read.
- An iterator is like a bookmark — it remembers exactly where you stopped reading.
- Calling iter() on a book gives you a fresh bookmark starting at page one.
The Iterator Protocol
The iterator protocol is simply a contract: any object that implements two special methods, __iter__() and __next__(), can be used as an iterator anywhere Python expects one — in a for loop, inside list(), or with next().
__iter__()
Returns the iterator object itself. This is what makes an iterator also count as iterable.
__next__()
Returns the next value in the sequence. Raises StopIteration when there are no more values left.
This is why you can pass an iterator into a for loop directly, and why calling iter() on something that is already an iterator just gives you back the same object.
Using iter() and next() Manually
The built-in functions iter() and next() let you step through the iterator protocol by hand, which is a great way to see exactly what is happening.
colors = ["red", "green", "blue"]
color_iterator = iter(colors)
print(next(color_iterator))
print(next(color_iterator))
print(next(color_iterator))red
green
blueEach call to next() moves the iterator forward by exactly one item and remembers its position for the next call. Once every item has been produced, calling next() again raises an error.
StopIteration
StopIteration is a special exception that an iterator raises to signal "there is nothing left to give you." A for loop catches this exception automatically and simply ends the loop — you never see it unless you are calling next() manually.
numbers = iter([1, 2])
print(next(numbers))
print(next(numbers))
print(next(numbers)) # nothing left!1
2
Traceback (most recent call last):
...
StopIterationIf you are calling next() yourself, you can catch this exception, or provide a default value: next(numbers, "no more items").
Writing a Custom Iterator
You can build your own iterator by writing a class that implements both __iter__() and __next__(). Here is a simple iterator that counts up from a starting number to an ending number, similar to a limited version of range().
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for number in Countdown(5):
print(number)5
4
3
2
1Notice how __next__() decides when to stop by raising StopIteration once self.current reaches zero. The __iter__() method simply returns self, since the object already knows how to produce values.
countdown = Countdown(3)
iterator = iter(countdown)
print(next(iterator))
print(next(iterator))
print(next(iterator))3
2
1How for-loops Use Iterators Under the Hood
A Python for loop is really just shorthand for a while loop built on top of iter() and next(). Writing it out manually helps demystify exactly what is happening every time you write "for item in something".
fruits = ["apple", "banana", "cherry"]
# This for loop...
for fruit in fruits:
print(fruit)
# ...is equivalent to this:
iterator = iter(fruits)
while True:
try:
fruit = next(iterator)
except StopIteration:
break
print(fruit)apple
banana
cherry
apple
banana
cherryCommon Mistakes
- Confusing an iterable (like a list) with an iterator — a list itself has no __next__().
- Forgetting that an iterator gets exhausted — once it reaches the end, it stays empty; you must call iter() again to start over.
- Forgetting to raise StopIteration inside a custom __next__(), which causes an infinite loop.
- Assuming iter(some_list) and some_list are the same object — iter() creates a fresh, independent iterator.
numbers = [1, 2, 3]
iterator = iter(numbers)
print(list(iterator)) # consumes the iterator
print(list(iterator)) # already exhausted![1, 2, 3]
[]Best Practices
- Use built-in iterables (list, tuple, dict, range) whenever possible instead of writing custom iterators from scratch.
- Always raise StopIteration in __next__() once the sequence is finished.
- Have __iter__() return self on iterator classes so the object works directly in for loops.
- Prefer a generator function (next lesson) over a full iterator class for simple cases — it requires far less boilerplate.
- Remember that iterators are single-use; create a new one if you need to loop again.
Frequently Asked Questions
Is every iterator also an iterable?
Yes. Every iterator implements __iter__() (which returns itself), so it satisfies the definition of an iterable too. The reverse is not true — not every iterable is an iterator.
Why does a list not have a __next__() method?
A list is a container of data, not a stateful "cursor" over that data. Calling iter() on it creates a separate iterator object that keeps track of the current position, leaving the list itself unchanged.
What happens if I call next() after StopIteration has already been raised?
The iterator continues to raise StopIteration on every subsequent call — it does not reset.
Do dictionaries and sets support the iterator protocol?
Yes. Calling iter() on a dict iterates over its keys, and iter() on a set iterates over its elements, both following the same protocol as lists.
How do generators relate to iterators?
A generator is a convenient, automatic way to create an iterator. Every generator object follows the same iterator protocol — it just implements __iter__() and __next__() for you behind the scenes, as you will see in the next lesson.
Key Takeaways
- An iterable is anything you can loop over; an iterator is the object that tracks position and produces values one at a time.
- The iterator protocol consists of two methods: __iter__() and __next__().
- iter() converts an iterable into an iterator; next() pulls the next value from it.
- StopIteration signals the end of an iterator, and for loops catch it automatically.
- You can build a custom iterator by implementing __iter__() (returning self) and __next__() (raising StopIteration when done).
- A for loop is essentially a while loop wrapped around iter() and next().
Summary
Iterators are the engine that powers every for loop in Python. By separating "the data" (an iterable) from "the cursor moving through the data" (an iterator), Python gives you a single, consistent way to loop over lists, strings, files, and even your own custom objects.
In this lesson, you learned the difference between iterables and iterators, worked through the __iter__ / __next__ protocol, saw StopIteration in action, and built a custom iterator class from scratch. Next, you will see how generators let you get all these same benefits with far less code.
- You can distinguish an iterable from an iterator.
- You understand and can implement the __iter__ / __next__ protocol.
- You know how and when StopIteration is raised.
- You are ready to learn about generators.