Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Python

Python Generators: How They Save Memory Without Magic

Learn how Python generators use a simple memory pattern to process large datasets without hogging RAM, with practical examples for log files and streaming pipelines.

July 2026 5 min read 3 views 0 hearts

I remember the first time I hit a MemoryError while processing a CSV file. The file wasn't even that large—maybe 500MB—but my naive code tried to load everything into a list at once. That's when I discovered Python generators, and honestly, it felt like a superpower.

Generators aren't about writing less code. They're about changing how Python handles memory under the hood. Let me show you what I mean.

The Problem: Lists Hog Memory

Here's a simple example that most beginners write:

def get_squares(n):
    squares = []
    for i in range(n):
        squares.append(i * i)
    return squares

# This creates a list of 10 million integers in memory
squares = get_squares(10_000_000)

On my machine, that list takes about 80MB of RAM. On a server with limited memory, this can crash your application. The issue is that get_squares returns the entire list at once.

The Generator Pattern: One Item at a Time

Now look at the generator version:

def generate_squares(n):
    for i in range(n):
        yield i * i

# This doesn't create all 10 million values at once
squares = generate_squares(10_000_000)

The memory usage? Almost nothing. A generator object itself takes only about 56 bytes, regardless of how many items it can produce. The difference is that a generator computes each value on demand and forgets it.

That's the core insight: generators trade memory for computation time. They recompute rather than store.

How Generators Pattern Memory

When you use a generator, Python doesn't allocate memory for all the values. Instead, it:

  1. Creates a generator object (tiny)
  2. Stores only the function's current state (where it paused, local variables)
  3. Computes the next value only when you call next() or iterate

This has a beautiful consequence for memory-intensive operations.

Real Example: Processing Log Files

At PythonSkillset, we once had to analyze 2GB of server logs. Here's the naive approach:

def load_logs(filepath):
    with open(filepath) as f:
        return [line.strip() for line in f]  # 2GB in memory!

logs = load_logs("server.log")
error_count = sum(1 for log in logs if "ERROR" in log)

This crashed our development server twice. The generator fix was simple:

def load_logs_generator(filepath):
    with open(filepath) as f:
        for line in f:
            yield line.strip()

logs = load_logs_generator("server.log")
error_count = sum(1 for log in logs if "ERROR" in log)

Same result, but the memory usage dropped from 2GB to about 8KB. The generator reads one line, processes it, then moves on. The line object is garbage collected immediately.

The Memory Pattern to Remember

Generators follow a simple memory pattern:

  • Input: One item from a source (file, database cursor, network stream)
  • Processing: Transform that one item
  • Output: Yield it, then forget it

This works because generators can be chained:

def read_lines(filepath):
    with open(filepath) as f:
        for line in f:
            yield line

def filter_errors(lines):
    for line in lines:
        if "ERROR" in line:
            yield line

def parse_errors(errors):
    for error in errors:
        # Extract timestamp, code, message
        parts = error.split(" | ")
        yield {"time": parts[0], "code": parts[1], "msg": parts[2]}

# Pipeline uses constant memory
errors = parse_errors(filter_errors(read_lines("server.log")))

Each generator in the pipeline holds only one item at a time. You can process terabytes of data with a few hundred bytes of memory.

When Generators Don't Help

Generators aren't always better. If you need random access to items (like data[5000]), a list is better. Also, if you're processing small datasets (under 10,000 items), the overhead of generator function calls might actually be slower than a list.

But for large datasets, streaming data, or when you don't need all values simultaneously, generators are the memory-efficient choice.

The Takeaway

Generators aren't complex once you understand the memory pattern. They don't store everything. They compute on demand. That's it.

Next time you write a function that builds a large list, ask yourself: "Do I really need all these values at once?" If the answer is no, make it a generator. Your memory usage will thank you.

And if you're working with files, databases, or network streams? Always use generators. They transform memory-hungry operations into efficient, streaming pipelines that scale naturally.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.