Skip to content

Generators and Iterators

Generators and iterators allow Python to produce values one at a time (lazy evaluation) instead of creating entire collections in memory.

This is critical when:

  • Data is large (files, streams, APIs)
  • Values are infinite or unknown
  • You only need partial results

An iterable is any object that can be looped over.

Examples:

  • list
  • tuple
  • string
  • set
  • dictionary
  • file
nums = [1, 2, 3]
for n in nums:
print(n)

An iterator is an object that:

  • Keeps track of position
  • Produces next value on demand

An object is an iterator if it implements:

  • __iter__() → returns iterator object
  • __next__() → returns next value
  • Raises StopIteration when done

graph TD A[Iterable] --> B[iter] B --> C[Iterator] C --> D[next] D --> E[Value] C --> F[StopIteration]

nums = [10, 20, 30]
it = iter(nums)
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
# next(it) -> StopIteration

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 num in CountDown(3):
print(num)

Generators are a simpler way to create iterators.

Instead of writing a class, you use yield.


  • return → ends function
  • yield → pauses function and saves state

def generate_numbers(n):
i = 1
while i <= n:
yield i
i += 1
gen = generate_numbers(3)
for num in gen:
print(num)

graph TD Call --> GeneratorObject GeneratorObject --> StartExecution StartExecution --> Yield Yield --> Pause Pause --> Resume Resume --> Yield Yield --> StopIteration

  • Function does NOT execute immediately
  • Runs only when next() is called
  • State is preserved between calls

# List (memory heavy)
nums = [x*x for x in range(1000000)]
# Generator (memory efficient)
nums = (x*x for x in range(1000000))

Generator:

  • Computes values only when needed
  • Uses constant memory

Compact syntax similar to list comprehensions.

gen = (x * 2 for x in range(5))
for val in gen:
print(val)

Generators can produce infinite sequences.


def infinite_counter():
num = 1
while True:
yield num
num += 1
counter = infinite_counter()
for _ in range(5):
print(next(counter))

1
2
3
4
5

graph TD Start --> Yield1 Yield1 --> Resume Resume --> Yield2 Resume --> Yield3 Resume --> InfiniteLoop

Generators can receive input using send().


def accumulator():
total = 0
while True:
value = yield total
total += value
acc = accumulator()
print(next(acc)) # start generator
print(acc.send(10)) # 10
print(acc.send(5)) # 15


When close() is called:

  • GeneratorExit is raised inside generator
  • You can clean up resources

def resource_manager():
print("Resource opened")
count = 0
try:
while True:
yield count
count += 1
except GeneratorExit:
print(f"Cleaning up... used {count} times")
gen = resource_manager()
print(next(gen))
print(next(gen))
gen.close()

Resource opened
0
1
Cleaning up... used 2 times

graph TD Start --> Yield Yield --> Resume Resume --> Yield Close --> GeneratorExit GeneratorExit --> Cleanup Cleanup --> Stop

def order_processor():
total_orders = 0
print("System Ready")
try:
order = yield
while True:
print(f"Processing: {order}")
total_orders += 1
order = yield
except GeneratorExit:
print(f"System Shutdown. Total orders: {total_orders}")
processor = order_processor()
next(processor) # start
processor.send("Pizza")
processor.send("Burger")
processor.close()

Used to simplify nested generators


def chain():
for x in [1, 2]:
yield x
for y in [3, 4]:
yield y

def chain():
yield from [1, 2]
yield from [3, 4]