What are Generators in Python?

A generator in Python is a special type of iterable that generates values on the fly instead of storing them in memory. It is created using functions with the yield keyword, making them more memory-efficient than lists.

Why Use Python Generators?

Memory Efficiency – Saves memory by generating values when needed
Faster Execution – Avoids unnecessary computations for unused values
Useful for Large Datasets – Handles infinite or large sequences efficiently

Difference Between Generators and Regular Functions

FeatureRegular FunctionGenerator Function
Returnsreturn statementyield statement
Memory UsageStores all values in memoryGenerates values one by one
ExecutionRuns once and exitsCan pause and resume
IterationCannot be resumedCan continue from the last yield

Basic Syntax of a Generator

A generator function uses the yield keyword to return values one at a time, instead of returning them all at once.

def my_generator():
    yield 1
    yield 2
    yield 3

gen = my_generator()
print(next(gen))  # Output: 1
print(next(gen))  # Output: 2
print(next(gen))  # Output: 3

💡 Best Practice: Always use yield instead of return when dealing with large sequences.

Step-by-Step Guide with Examples

1. Creating a Simple Generator

Example: Generator That Yields Numbers

def number_generator():
    for i in range(1, 4):
        yield i

gen = number_generator()
print(next(gen))  # Output: 1
print(next(gen))  # Output: 2
print(next(gen))  # Output: 3

💡 Best Practice: Use yield inside loops for better readability and efficiency.


2. Using Generators in a Loop

Instead of calling next(), you can use a for loop to iterate through a generator.

def count_down(n):
    while n > 0:
        yield n
        n -= 1

for num in count_down(5):
    print(num)  # Output: 5, 4, 3, 2, 1

💡 Best Practice: Use for loops with generators to automatically handle iteration.


3. Infinite Generators

Generators can be used to generate infinite sequences without using excessive memory.

Example: Infinite Fibonacci Sequence

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
for _ in range(10):
    print(next(fib), end=" ")  # Output: 0 1 1 2 3 5 8 13 21 34

💡 Best Practice: Use infinite generators when streaming large data sets.


4. Generator Expressions

Similar to list comprehensions, generator expressions provide a compact way to create generators.

Example: Generate Squares of Numbers

squares = (x**2 for x in range(5))

print(next(squares))  # Output: 0
print(next(squares))  # Output: 1
print(list(squares))  # Output: [4, 9, 16]

💡 Best Practice: Use generator expressions when you need a lightweight alternative to list comprehensions.


5. Chaining Generators

Generators can be combined to process data in steps.

Example: Filter Even Numbers from a Generator

def numbers():
    for i in range(10):
        yield i

def even_numbers(gen):
    for num in gen:
        if num % 2 == 0:
            yield num

evens = even_numbers(numbers())
print(list(evens))  # Output: [0, 2, 4, 6, 8]

💡 Best Practice: Chain multiple generators for complex data pipelines.


6. Generator vs List – Memory Comparison

Example: Memory Efficiency of Generators vs Lists

import sys

# List storing 1 million numbers
num_list = [x for x in range(1_000_000)]
print(sys.getsizeof(num_list), "bytes")  # Large memory usage

# Generator storing 1 million numbers
num_gen = (x for x in range(1_000_000))
print(sys.getsizeof(num_gen), "bytes")  # Much smaller memory usage

💡 Best Practice: Use generators instead of lists for large data processing.

When to Use Generators?

Large Datasets – Processing big files, logs, or databases
Streaming Data – APIs, real-time data processing
Memory Optimization – Generating values one at a time

🚫 Avoid generators when you need random access to elements since they do not support indexing.