🔁 Python Iterators

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.

🐍 Pythonbasic_iterator.py
numbers = [10, 20, 30]
my_iterator = iter(numbers)

print(next(my_iterator))
print(next(my_iterator))
print(next(my_iterator))

Output

10 20 30

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

🐍 Pythoniterable_vs_iterator.py
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 True

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

AspectIterableIterator
DefinitionAny object that can be looped overObject that produces values one at a time
Required methods__iter__() only__iter__() and __next__()
Exampleslist, tuple, str, dict, setObject returned by iter(), file objects, generators
Remembers position?❌ No✅ Yes
Can be reused?✅ Yes — creates a new iterator each time❌ No — exhausted after one full pass
Created viaAlready exists as a collectioniter(iterable)

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.

🐍 Pythoniter_next_example.py
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 StopIteration

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

🐍 Pythonnext_with_default.py
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 items

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

🐍 Pythonfor_loop_manual.py
fruits = ["apple", "banana"]

for fruit in fruits:
    print(fruit)

Is functionally identical to manually writing this expanded version:

🐍 Pythonfor_loop_expanded.py
fruits = ["apple", "banana"]
fruit_iterator = iter(fruits)

while True:
    try:
        fruit = next(fruit_iterator)
    except StopIteration:
        break
    print(fruit)

Output

apple banana

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

📋 for item in iterable:Loop begins
initialize
🔄 Call iter(iterable)Get an iterator object
request value
➡️ Call next(iterator)Request next value
check result
❓ StopIteration raised?Any values left?
value returned
🖨️ Run Loop BodyUse the returned value
loop again
🏁 Exit LoopIteration complete

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.

🐍 Pythoncustom_iterator.py
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 5

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

🐍 Pythoncustom_iterator_builtins.py
print(list(CountUpTo(4)))
print(sum(CountUpTo(4)))

Output

[1, 2, 3, 4] 10

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

🐍 Pythonsingle_use_problem.py
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.

🐍 Pythonreusable_iterable.py
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.

🐍 Pythongenerator_example.py
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 5

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

🐍 Pythonitertools_example.py
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.

FeatureClass-Based IteratorGenerator Function
SyntaxFull class with __iter__ and __next__Regular function using yield
Lines of codeTypically 8-15 linesTypically 3-6 lines
State managementManual — instance attributesAutomatic — handled by Python
Memory usageLow — one value at a timeLow — one value at a time
ReusabilityRequires separate iterable wrapper classCall the function again for a fresh generator
Best forComplex custom objects with extra methodsSimple, quick lazy sequences
StopIteration handlingMust raise manuallyAutomatic when function returns

Advantages and Disadvantages of Using Iterators

✅ Advantages
Memory EfficientIterators produce one value at a time rather than holding an entire sequence in memory, making them ideal for very large or infinite data sources.
Uniform InterfaceAny object implementing __iter__ and __next__ works seamlessly with for loops and built-in functions like list(), sum(), and max().
Supports Infinite SequencesSince values are computed lazily, iterators can represent sequences with no defined end, such as itertools.count().
Enables Lazy EvaluationComputation only happens when a value is actually requested, avoiding wasted work on values that are never used.
Foundation for GeneratorsUnderstanding the iterator protocol makes generators — Python's simpler, more common tool for lazy sequences — far easier to understand.
❌ Disadvantages
Single-Pass by DefaultMost iterators are exhausted after one full traversal and cannot be reset without recreating them from the original iterable.
No Direct IndexingUnlike a list, you cannot access the 'third value' of an iterator directly — values are only available in sequence, one at a time.
More Verbose for Simple CasesWriting a full class-based iterator requires more boilerplate code than achieving the same result with a generator function.
Harder to DebugSince values don't exist until requested, inspecting an iterator's full contents mid-debugging (without consuming it) requires extra care, such as using itertools.tee().

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))?

Easy

2. Is calling iter() on a list that is already an iterator necessary, or is a list itself already an iterator?

Easy

3. Write a custom iterator class that produces only even numbers up to a given limit.

Medium

4. Why does looping over the same exhausted iterator object twice in a row produce no output the second time?

Medium

5. What is the output of: print(list(itertools.islice(itertools.count(5, 2), 4)))?

Medium

6. Explain why file objects in Python are considered iterators.

Hard

7. How would you convert a generator function into an equivalent class-based iterator?

Hard

8. What is the difference between itertools.chain() and simply concatenating two lists with +?

Hard

Conclusion — 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.

Your GoalRecommended Approach
Loop over a list, tuple, or string✅ Use a plain for loop
Manually control iteration step by step✅ Use iter() and next()
Build a simple custom lazy sequence✅ Use a generator function with yield
Build a complex reusable custom iterable✅ Use separate iterable and iterator classes
Represent an infinite sequence✅ Use itertools.count() or a custom __next__()
Combine or slice iterators efficiently✅ Use itertools.chain() or islice()
Read a very large file efficiently✅ Loop directly over the file object

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

Frequently Asked Questions (FAQ)