LearnContact
Lesson 4225 min read

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.

What You Will Learn
  • 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__().

Checking the difference
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 iterator
Output
True
False
True
Quick Mental Model
  • 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.

Manual iteration
colors = ["red", "green", "blue"]

color_iterator = iter(colors)

print(next(color_iterator))
print(next(color_iterator))
print(next(color_iterator))
Output
red
green
blue

Each 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.

Exhausting an iterator
numbers = iter([1, 2])

print(next(numbers))
print(next(numbers))
print(next(numbers))  # nothing left!
Output
1
2
Traceback (most recent call last):
  ...
StopIteration
Handling StopIteration Manually

If 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().

A custom Countdown iterator
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)
Output
5
4
3
2
1

Notice 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.

Using the iterator manually
countdown = Countdown(3)
iterator = iter(countdown)

print(next(iterator))
print(next(iterator))
print(next(iterator))
Output
3
2
1

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

What a for loop is really doing
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)
Output
apple
banana
cherry
apple
banana
cherry
for loop calls iter(fruits)
gets back an iterator object
repeatedly calls next(iterator)
each call returns the next item
StopIteration is raised and silently caught
the loop ends automatically

Common Mistakes

Avoid These 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.
An iterator only works once
numbers = [1, 2, 3]
iterator = iter(numbers)

print(list(iterator))  # consumes the iterator
print(list(iterator))  # already exhausted!
Output
[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.

Lesson 42 Completed
  • 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.
Next Lesson →

Generators