← Back to Chapters

Python Generators

? Python Generators

⚡ Quick Overview

A generator in Python is a special kind of function that returns an iterator and lets you produce values one at a time using yield. Instead of creating and storing an entire sequence in memory, a generator creates each value only when it is requested (lazy evaluation), making it very memory-efficient, especially for large datasets or streams of data.

? Key Concepts

  • Generator function – a function that uses yield instead of return.
  • Generator object – the iterator returned when you call a generator function.
  • Lazy evaluation – values are produced only when requested.
  • Generator expression – a compact way to create generators using parentheses.
  • next() and StopIteration – used to manually pull values from a generator.
  • One-time use – once a generator is exhausted, it cannot be reused.

? Syntax and Theory

A generator function looks like a normal function but uses yield to produce a sequence of values. Each time the generator yields, its state is saved, and execution resumes from the same point when the next value is requested.

A generator expression is similar to a list comprehension, but uses parentheses instead of square brackets. It returns a generator object that produces items one by one.

The built-in next() function can be used to manually advance a generator. When there are no more values to produce, the generator raises a StopIteration exception internally.

? Code Examples

? Generator Function with yield

? View Generator Function Example
# generator that yields numbers 0 to 4
def my_generator():
    for i in range(5):
        yield i

# create generator object
gen = my_generator()

# iterate and print values
for value in gen:
    print(value)

? Generator Expression

? View Generator Expression Example
# generator expression for squares
gen_exp = (x * x for x in range(5))

# iterate through generator
for value in gen_exp:
    print(value)

⏭️ Using next() with a Generator

? View next() Example
gen = (x * x for x in range(3))
print(next(gen))  # 0
print(next(gen))  # 1
print(next(gen))  # 4
# next(gen) now will raise StopIteration

? Output and Explanation

? Understanding the Results

  • In my_generator(), the values 0 1 2 3 4 are printed one by one. The generator yields each i, and the for loop consumes those values.
  • In the generator expression example, the squares 0 1 4 9 16 are printed. The values are computed lazily as the loop iterates.
  • In the next() example, three calls to next(gen) return 0, 1, and 4. A fourth call would raise StopIteration because the generator has no more items to yield.

Notice that at no point is a full list stored in memory. Each value is generated only when needed, which is the core advantage of using generators.

✅ Tips & Best Practices

  • Use generators when working with large datasets or streams of data to save memory.
  • Prefer generator expressions when you only need to iterate once and don’t need random access.
  • Remember that generator functions must use yield, not return for multiple values.
  • Generators are exhausted after use—create a new generator if you need to iterate again.
  • Handle StopIteration if you manually use next(), or simply rely on a for loop.

? Try It Yourself

  • Create a generator function that yields even numbers up to 20.
  • Write a generator expression that produces cubes of numbers from 1 to 10.
  • Use next() a few times on a generator and then switch to a for loop to see how it behaves when exhausted.
  • Compare memory usage between a list comprehension and a generator expression for a very large range.