Python Iterators — Complete Guide with Examples
A complete beginner-friendly guide to Python iterators — iterable vs iterator, iter(), next(), StopIteration, building custom iterator classes, and the itertools module.
Last Updated
May 2026
Read Time
20 min
Level
Beginner
What is an Iterator in Python?
An iterator in Python is an object that represents a stream of data — it produces one value at a time, on demand, and remembers exactly where it left off between calls. Every for loop you have ever written in Python — over a list, string, dictionary, or file — is secretly powered by an iterator behind the scenes.
Formally, Python defines an iterator as any object that implements two special methods: __iter__(), which returns the iterator itself, and __next__(), which returns the next value in the sequence — or raises a StopIteration exception when there are no more values left. This pair of methods together is called the iterator protocol.
Iterators are one of Python's most elegant and quietly powerful features. They allow you to process sequences of unknown or even infinite length without loading the entire sequence into memory at once — a capability that becomes essential when working with huge files, network streams, or generated data.
numbers = [10, 20, 30]
my_iterator = iter(numbers)
print(next(my_iterator))
print(next(my_iterator))
print(next(my_iterator))Output
10 20 30Iterable vs Iterator — The Most Important Distinction
Beginners frequently confuse an iterable with an iterator — the two terms sound similar but describe fundamentally different things. Understanding this distinction is the single most important concept in this entire topic.
An iterable is any object you can loop over — a list, tuple, string, set, or dictionary. Technically, it's any object that implements __iter__(), which returns a fresh iterator. An iterator is the object actually doing the work of producing values one at a time — it implements both __iter__() and __next__(), and it keeps track of its current position, which an iterable itself does not do.
numbers = [1, 2, 3]
print(hasattr(numbers, '__iter__'))
print(hasattr(numbers, '__next__'))
my_iter = iter(numbers)
print(hasattr(my_iter, '__iter__'))
print(hasattr(my_iter, '__next__'))Output
True False True TrueThis is the key takeaway: a list is iterable (it knows how to produce an iterator), but a list is not itself an iterator (it has no __next__() method and no memory of position). Once you call iter(numbers), you get an actual iterator that does have both methods.
The iter() and next() Built-in Functions
Python provides two built-in functions that form the practical entry point to the entire iterator system: iter() converts an iterable into an iterator, and next() retrieves the following value from that iterator.
colors = ("red", "green", "blue")
color_iterator = iter(colors)
print(next(color_iterator))
print(next(color_iterator))
print(next(color_iterator))
print(next(color_iterator))Output
red green blue StopIterationOnce every value has been produced, calling next() one more time raises a StopIteration exception — this signals to whatever is consuming the iterator (like a for loop) that there is nothing left to retrieve. Crucially, once exhausted, an iterator cannot be reset or reused — you must call iter() again on the original iterable to start over.
next() also accepts an optional default value, letting you avoid the StopIteration exception entirely — similar in spirit to how dict.get() avoids a KeyError.
numbers = iter([1, 2])
print(next(numbers, "No more items"))
print(next(numbers, "No more items"))
print(next(numbers, "No more items"))Output
1 2 No more itemsWhat a for Loop Actually Does Behind the Scenes
Every for loop in Python is simply syntactic sugar around the iterator protocol. When you write for item in my_list:, Python performs a precise, repeatable sequence of steps automatically, entirely hidden from view.
fruits = ["apple", "banana"]
for fruit in fruits:
print(fruit)Is functionally identical to manually writing this expanded version:
fruits = ["apple", "banana"]
fruit_iterator = iter(fruits)
while True:
try:
fruit = next(fruit_iterator)
except StopIteration:
break
print(fruit)Output
apple bananaThis is precisely why any object implementing the iterator protocol — whether a built-in list, a file object, or your own custom class — can be used directly in a for loop, without Python needing any special-case handling for each type.
How the Iterator Protocol Works — Flowchart
The diagram below traces the exact sequence of events that occurs every single time a for loop runs over any iterable in Python.
Code Execution Flow — from source to output
Key insight: iter() is called exactly once at the start of the loop, while next() is called repeatedly until StopIteration is raised, at which point Python catches it internally and exits the loop cleanly — the exception never propagates up to your own code in a normal for loop.
Building Your Own Custom Iterator Class
Any Python class can become an iterator by implementing both __iter__() and __next__(). This is a foundational technique for building custom, memory-efficient sequences — for example, a counter that counts up to a limit.
class CountUpTo:
def __init__(self, limit):
self.limit = limit
self.current = 0
def __iter__(self):
return self
def __next__(self):
if self.current >= self.limit:
raise StopIteration
self.current += 1
return self.current
counter = CountUpTo(5)
for number in counter:
print(number)Output
1 2 3 4 5Walking through this class: __init__() sets up the starting state — a limit and a current counter starting at 0. __iter__() simply returns self, since the object itself already knows how to produce values. __next__() is where the real logic lives: it checks whether the limit has been reached, and either raises StopIteration or increments and returns the next number.
Because CountUpTo implements the full iterator protocol, it works seamlessly not just in for loops, but with any built-in function that expects an iterable — list(), sum(), max(), and more.
print(list(CountUpTo(4)))
print(sum(CountUpTo(4)))Output
[1, 2, 3, 4] 10Why Custom Iterators Are Often Single-Use
A subtle limitation of the CountUpTo class above is that once exhausted, it cannot be reused — its __iter__() returns self, which is the same object with current already at its limit.
counter = CountUpTo(3)
print(list(counter))
print(list(counter))Output
[1, 2, 3] []The second call returns an empty list because the same exhausted iterator object was reused. This is exactly how many of Python's own built-in iterators behave too — a file object, once fully read, produces no more lines even if you try to iterate it again without reopening it.
To make a class truly reusable across multiple loops, separate the roles: make the class an iterable (with __iter__() that returns a brand-new iterator object each time) rather than an iterator itself.
class CountUpToIterable:
def __init__(self, limit):
self.limit = limit
def __iter__(self):
return CountUpTo(self.limit)
numbers = CountUpToIterable(3)
print(list(numbers))
print(list(numbers))Output
[1, 2, 3] [1, 2, 3]Now every call to iter(numbers) creates a fresh CountUpTo object with its own independent state, so the same CountUpToIterable object can be looped over as many times as needed — this is precisely how built-in lists and tuples behave.
Generators — A Simpler Way to Build Iterators
Writing a full class with __iter__() and __next__() works, but Python offers a dramatically simpler shortcut for building iterators: generator functions, which use the yield keyword instead of return.
def count_up_to(limit):
current = 1
while current <= limit:
yield current
current += 1
for number in count_up_to(5):
print(number)Output
1 2 3 4 5This single function replaces the entire CountUpTo class from earlier. Every time yield executes, the function's state is paused, its value is handed to whoever called next(), and execution resumes exactly where it left off on the following call. Python automatically implements __iter__() and __next__() for you behind the scenes — this deep topic deserves its own dedicated guide, linked below.
The itertools Module — Iterator Building Blocks
Python's standard library ships with itertools, a module packed with fast, memory-efficient functions for creating and combining iterators — covering infinite sequences, combinatorics, and grouping operations, all without ever loading a full sequence into memory.
- ▶
itertools.count(start, step)— Produces an infinite sequence of evenly spaced numbers, starting from start. - ▶
itertools.cycle(iterable)— Repeats the given iterable's elements infinitely, looping back to the start once exhausted. - ▶
itertools.repeat(value, times)— Repeats a single value a specified number of times, or forever if times is omitted. - ▶
itertools.chain(iter1, iter2)— Combines multiple iterables into a single continuous iterator, without creating an intermediate combined list. - ▶
itertools.islice(iterable, stop)— Slices an iterator lazily, useful for taking a limited number of values from an otherwise infinite iterator. - ▶
itertools.permutations(iterable)— Generates all possible orderings of the given iterable's elements. - ▶
itertools.combinations(iterable, r)— Generates all possible r-length combinations of elements, without regard to order.
import itertools
counter = itertools.count(10, 5)
print(next(counter))
print(next(counter))
print(next(counter))
limited = itertools.islice(itertools.count(1), 5)
print(list(limited))Output
10 15 20 [1, 2, 3, 4, 5]Note the use of islice() to safely take only the first five values from itertools.count(1) — since count() produces values infinitely, calling list() directly on it would run forever and never return.
How Iterators Work Internally — Memory Efficiency
The core motivation behind the iterator protocol is lazy evaluation — producing values one at a time, only when requested, rather than computing and storing an entire sequence upfront. This is what allows Python to represent sequences that would be impossible to hold in memory all at once, such as reading a 50 GB log file line by line.
When you open a file in Python, the file object itself is an iterator — calling next() on it (or looping over it with for line in file:) reads and returns exactly one line at a time from disk, never loading the entire file's contents into memory simultaneously. This is a direct, practical benefit of the iterator protocol that would be impossible if Python only supported fully materialized lists.
This same lazy-evaluation principle is why itertools.count() can represent an infinite sequence without crashing your program — no memory is allocated for values that haven't been requested yet, and the sequence simply continues producing values on demand, forever, until you explicitly stop consuming it.
Practice Iterators — Live Editor
Try modifying the code below to experiment with manual iteration. Add another next() call after the loop ends and observe the StopIteration exception.
Practice This Code — Live Editor
Iterator (Class-Based) vs Generator — Comparison
Both class-based iterators and generator functions produce values lazily one at a time, but they differ significantly in syntax, complexity, and typical use cases.
Advantages and Disadvantages of Using Iterators
Real-World Use Cases of Python Iterators
- ▶
📄 Reading Large Files Line by Line — Processing multi-gigabyte log files or CSV files uses the file object's built-in iterator to read one line at a time, keeping memory usage constant regardless of file size.
- ▶
🌐 Paginated API Responses — Custom iterator classes are commonly built to fetch and yield pages of results from a web API one at a time, hiding the pagination logic behind a simple for loop.
- ▶
📊 Streaming Data Pipelines — Data processing pipelines chain multiple iterators together, transforming and filtering records one at a time as they flow through, without ever materializing the full dataset in memory.
- ▶
🔢 Infinite ID or Token Generators — Systems that need to generate unique sequential IDs commonly use itertools.count() as an infinite, thread-safe-friendly counter.
- ▶
🧮 Custom Numerical Sequences — Mathematical sequences like Fibonacci numbers or prime numbers are frequently implemented as custom iterators or generators, since their length is unbounded.
- ▶
🗄️ Database Cursor Iteration — Database libraries return query results as an iterator over rows, fetching each row from the database only as it's requested rather than loading the entire result set upfront.
Python Iterators — Interview Questions
Practice Questions — Test Your Knowledge
1. What is the output of: it = iter([1, 2]); print(next(it)); print(next(it)); print(next(it))?
Easy2. Is calling iter() on a list that is already an iterator necessary, or is a list itself already an iterator?
Easy3. Write a custom iterator class that produces only even numbers up to a given limit.
Medium4. Why does looping over the same exhausted iterator object twice in a row produce no output the second time?
Medium5. What is the output of: print(list(itertools.islice(itertools.count(5, 2), 4)))?
Medium6. Explain why file objects in Python are considered iterators.
Hard7. How would you convert a generator function into an equivalent class-based iterator?
Hard8. What is the difference between itertools.chain() and simply concatenating two lists with +?
HardConclusion — Why Iterators Matter in Python
Iterators are the quiet engine powering nearly every loop you write in Python — from a simple for loop over a list, to reading a massive log file line by line, to processing an infinite stream of sensor data. Understanding the iterator protocol — __iter__() and __next__() — demystifies what Python is actually doing every time you iterate over anything.
The single most important distinction to internalize: an iterable is something you can loop over (like a list), while an iterator is the object actually doing the looping, one step at a time, remembering exactly where it is. Every iterable knows how to produce a fresh iterator; an iterator itself is typically single-use and gets exhausted after one full pass.
The natural next step in your Python journey is generators — the simpler, yield-based way to build iterators that you saw a preview of in this guide. Mastering generators will let you write memory-efficient, lazy code with a fraction of the boilerplate required for a full class-based iterator. Practice building your own iterator class in the live editor above until the protocol feels completely natural. 🐍