Web Analytics

Iterators

Advanced ~25 min read

An iterator is an object that implements the iterator protocol, allowing you to traverse through a sequence of values one at a time. Unlike lists or tuples that store all values in memory, iterators generate values on-demand, making them memory-efficient for large datasets. Understanding iterators unlocks powerful patterns for custom iteration behavior!

Iterables vs Iterators

An iterable is any object you can iterate over (lists, tuples, strings, dicts). An iterator is an object that produces the next value when you call next() on it. When you use a for loop, Python automatically converts the iterable into an iterator.

Output
Click Run to execute your code
Key Difference:
- Iterable: Has __iter__() method that returns an iterator
- Iterator: Has both __iter__() (returns self) and __next__() (returns next value)

All iterators are iterables, but not all iterables are iterators. Lists are iterables but not iterators until converted.

The Iterator Protocol

To create a custom iterator, you must implement two methods: __iter__() and __next__(). The __iter__() method returns the iterator object itself (or a new iterator object). The __next__() method returns the next value and raises StopIteration when there are no more items.

Output
Click Run to execute your code
Pro Tip: You can use the built-in iter() function to get an iterator from any iterable, and next() to manually advance through values. This gives you fine-grained control over iteration!

Creating Custom Iterators

Custom iterators let you define exactly how iteration works for your objects. This is powerful for lazy evaluation, infinite sequences, or custom traversal logic.

Output
Click Run to execute your code

Memory Efficiency of Iterators

Unlike lists that store all values in memory, iterators generate values on-demand. This makes them perfect for large datasets or infinite sequences that wouldn't fit in memory.

Output
Click Run to execute your code
Important: Once an iterator is exhausted (raises StopIteration), you can't reuse it. To iterate again, you need to create a new iterator by calling __iter__() or using iter() again. This is why you can only iterate over a file handle once!

Common Mistakes

1. Forgetting to raise StopIteration

# Wrong - will loop forever or return None
class Counter:
    def __iter__(self):
        self.count = 0
        return self
    
    def __next__(self):
        if self.count < 5:
            self.count += 1
            return self.count
        # Missing: raise StopIteration

# Correct - explicitly raise StopIteration
class Counter:
    def __iter__(self):
        self.count = 0
        return self
    
    def __next__(self):
        if self.count < 5:
            self.count += 1
            return self.count
        raise StopIteration  # Signal end of iteration

2. Trying to iterate over an exhausted iterator

# Wrong - iterator is exhausted after first loop
numbers = iter([1, 2, 3])
for n in numbers:
    print(n)

for n in numbers:  # This won't print anything!
    print(n)

# Correct - create a new iterator for each loop
numbers = [1, 2, 3]
for n in numbers:  # Automatically creates iterator
    print(n)

for n in numbers:  # Creates a new iterator
    print(n)

3. Confusing iterables with iterators

# Wrong - list is iterable but not an iterator
my_list = [1, 2, 3]
next(my_list)  # TypeError: 'list' object is not an iterator

# Correct - convert to iterator first
my_list = [1, 2, 3]
my_iterator = iter(my_list)
print(next(my_iterator))  # Works: 1
print(next(my_iterator))  # Works: 2

Task: Create a countdown iterator that counts down from a given number to 1, then raises StopIteration.

Requirements:

  • Implement a Countdown class with __iter__ and __next__ methods
  • Initialize with a starting number
  • Return numbers in descending order (start, start-1, ..., 1)
  • Raise StopIteration when countdown reaches 0
  • Test it with both a for loop and manual next() calls
Output
Click Run to execute your code
Show Solution
class Countdown:
    def __init__(self, start):
        self.start = start
        self.current = start
    
    def __iter__(self):
        self.current = self.start  # Reset for new iteration
        return self
    
    def __next__(self):
        if self.current > 0:
            value = self.current
            self.current -= 1
            return value
        else:
            raise StopIteration


# Test with for loop
print("Countdown from 5:")
for num in Countdown(5):
    print(num)

# Test with manual next()
print("\nManual iteration from 3:")
counter = Countdown(3)
it = iter(counter)
print(next(it))  # 3
print(next(it))  # 2
print(next(it))  # 1
# next(it) would raise StopIteration

Summary

  • Iterable: Object with __iter__() that can be looped over (lists, tuples, strings, dicts)
  • Iterator: Object with both __iter__() and __next__() that produces values on-demand
  • Iterator Protocol: Implement __iter__() (returns self) and __next__() (returns next value, raises StopIteration when done)
  • Memory Efficiency: Iterators generate values lazily, perfect for large datasets
  • One-time Use: Iterators are consumed after one pass; create new ones for multiple iterations
  • Built-in Functions: Use iter() to get an iterator from an iterable, next() to manually advance
  • StopIteration: Must raise this exception to signal end of iteration

What's Next?

Now you understand the iterator protocol! Next, we'll explore generators, which provide a simpler way to create iterators using the yield keyword. Generators are iterators under the hood, but Python handles all the __iter__ and __next__ boilerplate for you!