Iterators & Generators in Python
Iterators
An iterator is any object that implements __iter__() and __next__(). When you use a for loop, Python calls these methods behind the scenes.
# Built-in iterables
nums = [1, 2, 3]
it = iter(nums) # get iterator from list
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
# next(it) # StopIteration!
# for loop does this automatically
for n in [1, 2, 3]:
print(n)
# Custom iterator class
class CountUp:
def __init__(self, start: int, stop: int):
self.current = start
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.current > self.stop:
raise StopIteration
value = self.current
self.current += 1
return value
for n in CountUp(1, 5):
print(n) # 1 2 3 4 5
Generators with yield
A generator is a function that uses yield instead of return. It produces values one at a time, pausing between each — memory-efficient for large sequences.
def count_up(start: int, stop: int):
current = start
while current <= stop:
yield current # pause here, return value
current += 1 # resume here on next call
gen = count_up(1, 5)
print(next(gen)) # 1
print(next(gen)) # 2
for n in count_up(1, 5):
print(n) # 1 2 3 4 5
# Fibonacci generator — infinite sequence
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# Generator vs list — memory comparison
import sys
gen_size = sys.getsizeof(x for x in range(1_000_000))
list_size = sys.getsizeof([x for x in range(1_000_000)])
print(f"Generator: {gen_size} bytes") # ~200 bytes
print(f"List: {list_size} bytes") # ~8 MB!
Generator Expressions
Like list comprehensions but with parentheses — lazy evaluation, no memory overhead.
# List comprehension — creates entire list in memory
squares_list = [x**2 for x in range(10)]
# Generator expression — lazy, one value at a time
squares_gen = (x**2 for x in range(10))
# Use directly in functions
total = sum(x**2 for x in range(1, 101)) # sum of squares 1-100
print(total) # 338350
# Find first match without building full list
first_even = next(x for x in range(100) if x % 7 == 0 and x > 10)
print(first_even) # 14
# Chain generators (pipeline)
numbers = range(1, 20)
evens = (x for x in numbers if x % 2 == 0)
squared = (x**2 for x in evens)
print(list(squared)) # [4, 16, 36, 64, 100, 144, 196, 256, 324]
yield from
# yield from — delegate to another iterable
def flatten(nested):
for item in nested:
if isinstance(item, list):
yield from flatten(item) # recurse into sub-lists
else:
yield item
data = [1, [2, 3], [4, [5, 6]], 7]
print(list(flatten(data))) # [1, 2, 3, 4, 5, 6, 7]
# Combine multiple generators
def chain(*iterables):
for it in iterables:
yield from it
result = list(chain([1, 2], [3, 4], [5, 6]))
print(result) # [1, 2, 3, 4, 5, 6]
# itertools — powerful iterator tools
import itertools
# islice — take first n items from any iterable
fib_10 = list(itertools.islice(fibonacci(), 10))
print(fib_10) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# chain — combine iterables
combined = list(itertools.chain([1, 2], [3, 4], [5]))
print(combined) # [1, 2, 3, 4, 5]