Command Palette

Search for a command to run...

Iterators & Generators

Learn how to use Python's iteration protocol and build memory-efficient generators for real-world use

Understanding Iterators

  • An iterator is an object that implements __iter__() and __next__()
  • iter() function creates an iterator from an iterable
  • next() function retrieves the next value from an iterator
  • Raises StopIteration when no more items are available
# Using iter() and next()
my_list = [1, 2, 3, 4]
iterator = iter(my_list)

print(next(iterator))  # 1
print(next(iterator))  # 2
print(next(iterator))  # 3
print(next(iterator))  # 4

# Handling StopIteration
try:
    print(next(iterator))
except StopIteration:
    print("No more items")

Creating Custom Iterators

  • Implement __iter__() to return self
  • Implement __next__() to return the next value
  • Raise StopIteration when iteration is complete
  • Useful for custom iteration logic
# Custom iterator class
class Counter:
    def __init__(self, limit: int):
        self.limit = limit
        self.current = 0
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.current < self.limit:
            self.current += 1
            return self.current
        raise StopIteration

# Using custom iterator
counter = Counter(5)
for num in counter:
    print(num, end=" ")

Generator Functions

  • Use yield keyword instead of return
  • Automatically implements the iterator protocol
  • State is preserved between calls
  • More memory efficient than lists
from typing import Generator

# Simple generator function
def count_up_to(limit: int) -> Generator[int, None, None]:
    count = 1
    while count <= limit:
        yield count
        count += 1

# Using generator
counter = count_up_to(5)
for num in counter:
    print(num, end=" ")

# Generator with multiple yields
def fibonacci(n: int) -> Generator[int, None, None]:
    a, b = 0, 1
    for _ in range(n):
        yield a
        a, b = b, a + b

print("\nFibonacci:")
for fib in fibonacci(10):
    print(fib, end=" ")

Generator Benefits

  • Memory efficient – values are generated on demand
  • Can represent infinite sequences
  • Lazy evaluation – compute only when needed
  • Enables pipeline processing with multiple generators
from typing import Generator

# Infinite generator
def infinite_sequence() -> Generator[int, None, None]:
    num = 0
    while True:
        yield num
        num += 1

# Use with limit
gen = infinite_sequence()
for _ in range(5):
    print(next(gen), end=" ")

# Generator pipeline
def numbers() -> Generator[int, None, None]:
    for i in range(10):
        yield i

def squares(nums: Generator[int, None, None]) -> Generator[int, None, None]:
    for n in nums:
        yield n ** 2

def evens(nums: Generator[int, None, None]) -> Generator[int, None, None]:
    for n in nums:
        if n % 2 == 0:
            yield n

# Chain generators
result = evens(squares(numbers()))
print("\nEven squares:", list(result))

Real-life Use Case

  • Generators are especially useful when working with large data
  • They allow you to process data lazily without loading everything into memory
# Real-life use case: Reading large files line by line
from typing import Generator

def read_large_file(file_path: str) -> Generator[str, None, None]:
    with open(file_path, "r") as file:
        for line in file:
            yield line.strip()

# Process file lazily without loading it fully into memory
for line in read_large_file("data.txt"):
    print(line)