Python Iterators & Generators

๐Ÿ Python 3.12+ ๐ŸŸข Chapter 36 of 65 ๐Ÿ“‚ Phase 8: Advanced Python ๐Ÿ“… 2026 Edition
๐Ÿ“Œ Covered in this chapter: Iterable vs Iterator Protocol ยท __iter__() & __next__() ยท yield Keyword ยท State Suspension ยท Generator Expressions ยท Big Data Streams
Master lazy evaluation and streaming data pipelines in Python: in-depth understanding of the Iterator Protocol (__iter__ and __next__), stack-frame suspension with yield, generator expressions, and processing gigabytes of data in constant O(1) memory.
1The Iterator Protocol: Iterable vs Iterator Deep Dive

In Python, iteration is one of the most powerful and fundamental language features. Under the hood, iteration is governed by the formal Iterator Protocol consisting of two distinct roles:

1. What is an Iterable?

An Iterable is any Python object capable of returning its members one at a time. Examples include lists, strings, tuples, dictionaries, sets, and open file objects. An object qualifies as an iterable if it implements the __iter__() method (or __getitem__() with sequential integer indices).

2. What is an Iterator?

An Iterator is the stateful stream object that actually produces values during traversal. An iterator maintains an internal cursor in memory and must implement two methods:

  • __iter__(): Returns the iterator object itself.
  • __next__(): Returns the next item from the container. If no further items remain, it must raise the StopIteration exception.
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ THE PYTHON ITERATOR PROTOCOL โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ Iterable: data = [10, 20, 30] โ”‚ โ”‚ โ””โ”€โ”€ Calls it = iter(data) / data.__iter__() โ”‚ โ”‚ โ”‚ โ”‚ Iterator Stream Object (it): โ”‚ โ”‚ โ”œโ”€โ”€ next(it) -> 10 (Cursor advances to position 1) โ”‚ โ”‚ โ”œโ”€โ”€ next(it) -> 20 (Cursor advances to position 2) โ”‚ โ”‚ โ”œโ”€โ”€ next(it) -> 30 (Cursor advances to position 3) โ”‚ โ”‚ โ””โ”€โ”€ next(it) -> raises StopIteration! (Clean loop termination) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
๐Ÿ’ป Example 1: Manually Driving an Iterator with iter() and next()
# Demonstrating the Iterator Protocol manually step-by-step:
fruits = ["Apple", "Mango", "Banana"]

# Step 1: Obtain an iterator from the iterable list:
fruit_stream = iter(fruits)
print("Iterator Type:", type(fruit_stream))

# Step 2: Fetch elements one by one via next():
print("1st Fetch:", next(fruit_stream)) # 'Apple'
print("2nd Fetch:", next(fruit_stream)) # 'Mango'
print("3rd Fetch:", next(fruit_stream)) # 'Banana'

# Step 3: Stream is now exhausted; subsequent call raises StopIteration:
try:
    next(fruit_stream)
except StopIteration:
    print("๐Ÿ›‘ StopIteration Exception Caught: Stream fully exhausted!")
๐Ÿ” Under-the-Hood: How Python "for" Loops Work:

When you write for x in fruits:, Python executes three exact C-level steps: (1) calls iter(fruits), (2) enters an infinite loop repeatedly calling next(), and (3) catches StopIteration to terminate the loop cleanly without crashing!

2Generators & The "yield" Keyword (State Suspension Mechanics)

A Generator is a special function that produces a sequence of values lazily on demand. Unlike standard functions that compute everything up-front and return a complete list in RAM, generators calculate each item only when requested.

The Crucial Difference Between return and yield:

  • return: Computes the final value, destroys the function's local execution stack frame and local variables, and returns control to the caller.
  • yield: Pauses execution, freezes the entire execution stack frame in RAM (all local variable values, loop counters, and instruction pointers), sends the yielded value to the caller, and waits. When next() is called again, execution resumes immediately at the exact line after yield!
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ STACK-FRAME SUSPENSION WITH YIELD โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ def count_to_three(): โ”‚ โ”‚ n = 1 โ”‚ โ”‚ yield n โ”€โ”€>[1. Yields 1] โ”€โ”€>[FREEZES STACK: n=1, line=3] โ”‚ โ”‚ n += 1 โ”‚ โ”‚ yield n โ”€โ”€>[2. Resumes] โ”€โ”€>[Yields 2] โ”€โ”€>[FREEZES: n=2, line=5]โ”‚ โ”‚ n += 1 โ”‚ โ”‚ yield n โ”€โ”€>[3. Resumes] โ”€โ”€>[Yields 3] โ”€โ”€>[FREEZES: n=3, line=7]โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
๐Ÿ’ป Example 2: Generator Function with yield and State Suspension
# Generator function demonstrating state suspension:
def countdown_timer(start_seconds):
    """Yields countdown numbers lazily."""
    current = start_seconds
    while current > 0:
        print(f"  [Generator Internals] Yielding {current} and freezing state...")
        yield current
        current -= 1 # Resumes here on next call!
    print("  [Generator Internals] Countdown complete!")

print("--- Calling Generator Function ---")
# Calling the generator function returns a generator object instantly without executing code:
timer_gen = countdown_timer(3)
print("Generator Object Created:", timer_gen)

print("\n--- Iterating Through Values Lazily ---")
for sec in timer_gen:
    print(f"๐Ÿ‘‰ Received: {sec}s remaining")
๐Ÿ” Memory Comparison:

If start_seconds = 100,000,000, creating a list list(range(100_000_000)) would require ~800 Megabytes of RAM. The generator requires only ~120 Bytes because it produces numbers one at a time on the fly.

3Generator Expressions ((expr for x in seq)) vs List Comprehensions

A Generator Expression uses the exact same syntax as a list comprehension, but replaces square brackets [...] with parentheses (...).

FeatureList Comprehension [...]Generator Expression (...)
EvaluationEager (Calculates all elements immediately)Lazy (Calculates elements one-by-one on demand)
Memory$O(N)$ memory (Scales linearly with dataset size)$O(1)$ constant memory (~100 bytes regardless of size)
AccessIndexed random access (lst[5]) and slicingSequential streaming access only (via next() or loop)
ReusabilityCan be iterated multiple timesExhausted after a single iteration pass
๐Ÿ’ป Example 3: Memory Footprint Comparison & Aggregation Functions
import sys

# 1. Eager List Comprehension (Allocates memory for 1,000,000 items in RAM):
list_squares = [x ** 2 for x in range(1_000_000)]

# 2. Lazy Generator Expression (Allocates only lightweight generator state object):
gen_squares = (x ** 2 for x in range(1_000_000))

print(f"๐Ÿ“ฆ List Comprehension Memory: {sys.getsizeof(list_squares):,} bytes (~8.4 MB)")
print(f"โšก Generator Expression Memory:  {sys.getsizeof(gen_squares):,} bytes (Constantly tiny!)")

# Passing generator expressions directly into aggregation functions:
total_sum = sum(x ** 2 for x in range(1000)) # Parentheses can be omitted inside functions!
print(f"\nSum of squares up to 1000: {total_sum:,}")
๐Ÿ” Performance Best Practice:

When passing data into aggregation functions like sum(), max(), min(), or any(), always use a generator expression without square brackets to avoid creating unnecessary intermediate lists in RAM.

โš ๏ธ Common Developer Pitfall: Attempting to Re-Iterate Over an Exhausted Generator

Generators are one-way data streams. Once an iterator reaches the end and raises StopIteration, subsequent for-loops over that instance will execute 0 times. To re-iterate, you must instantiate a fresh generator.

๐Ÿ’ป Hands-on Interactive Practice Challenge

Write a generator fibonacci(limit) that yields Fibonacci numbers up to limit. Print all generated numbers in a single line.

Python 3 Practice Challenge โ–ถ Run in Compiler
def fibonacci(limit):
    a, b = 0, 1
    while a <= limit:
        yield a
        a, b = b, a + b

print("Fibonacci numbers up to 100:")
for num in fibonacci(100):
    print(num, end=" ")
print()
Run This Challenge in Online Python IDE โ†’
โ“ Frequently Asked Questions (FAQ)

Q What is the difference between yield and yield from in Python?

yield from iterable is used to delegate part of a generator's operations to another sub-generator or iterable, cleanly transparently forwarding values.

Q Can generators receive data from the caller while running?

Yes! You can pass values into a running generator using generator.send(value). Inside the generator, the yield expression evaluates to the received value.

Q Are generators thread-safe in Python?

Calling next() on the same generator instance simultaneously from multiple threads is not thread-safe without explicit synchronization (threading.Lock).

OC
Written by Our Compiler Technical Editorial Team
Reviewed for accuracy & tested on Python 3.12+ runtime ยท Last updated August 2026