Lazy Evaluation and Memory Efficiency

RoadmapsPython

Overview

Lazy evaluation is a strategy where an expression is not computed until its value is strictly needed. Instead of materializing an entire sequence in memory at once (like a list), Python can yield one item at a time upon request. nums = [1, 2, 3, 4, 5] lazy = (n * n for n in nums) # nothing computed yet print(next(lazy)) # 1 -- only the first square is computed print(next(lazy)) # 4 -- computed on demand, one at a time This acts like a streaming pipeline: `list(x for x in huge_file)` would load everything into memory at once, while iterating the generator directly processes one item, uses it, and discards it before pulling the next. The mental model: computing a result 'just in time' rather than 'all in advance', reducing both initial startup time and total memory consumption.

It prevents memory exhaustion when processing massive datasets (e.g., parsing a 50GB log file or querying millions of database rows) and allows processing to begin immediately without waiting for the full dataset to load.

Where used: Pandas chunking, Django QuerySets, SQLAlchemy cursors, File reading

Why learn this

Code walkthrough

def process():
    print('Computing...')
    return 42

lazy_result = (process() for _ in range(2))
print('Created')
first = next(lazy_result)
print('Done')

Focus: first = next(lazy_result)

Aha moment

nums = [1, 2, 3]
lazy_squares = (n * n for n in nums)
nums.clear()
print(next(lazy_squares, 'Empty'))

Prediction: What will be printed when we call `next()` on the generator expression after clearing the source list?

Common guess: 1

The generator expression evaluates lazily and fetches from the `nums` list when `next()` is called. Since the list was cleared beforehand, it yields the default 'Empty'.

Common mistakes

Recall questions

Understanding checks

Why would you choose a generator expression over a list comprehension when reading a massive log file line-by-line?

A generator expression yields one line at a time on demand, so it uses almost no memory no matter how large the file is. A list comprehension loads every line into memory at once, which can crash the program if the file is larger than available RAM.

A list comprehension materializes all items in memory at once, which could crash your program if the file is larger than available RAM. A generator expression evaluates lazily, yielding one line at a time and maintaining a low memory footprint regardless of the file size.

What happens when this code is executed?

[1, 2] []

Lazy iterators, like the one returned by a generator function, compute items on the fly and do not store them. Once exhausted by the first `list()` call, the iterator is empty, so the second call yields an empty list.

Practice tasks

Processing a large stream lazily

The `print_errors` function currently converts the `logs` generator into a list, which eagerly processes all entries and can cause a memory crash. Modify the function to process the `logs` lazily.

Challenge

Lazy batch filtering

You are given an iterator `records` that yields thousands of dictionaries representing API payloads. Each payload has a 'size' key. Create a lazy generator expression `large_records` that yields only the payloads where 'size' > 1000. Then use `next()` to return the very first large payload without processing the rest of the stream. Return `None` if no such payload exists.

Continue learning

Previous: Generator expressions vs list comprehensions

Previous: Iterator Protocol and StopIteration

Return to Python Roadmap