Python Deferred Computation — Core Concepts
Eager vs Deferred Execution
In eager execution, every expression is evaluated immediately. When you write results = [process(x) for x in big_list], Python processes every element right away and stores all results in memory.
In deferred (lazy) execution, computation is postponed until the result is actually consumed. The work happens incrementally, on demand.
The benefit: if you only need the first 10 results from a million-item dataset, deferred computation processes just those 10 and stops. Eager computation would process all one million first.
Generators: Python’s Primary Deferral Tool
Generators are functions that use yield instead of return. They produce values one at a time and pause execution between each:
def read_large_file(path):
with open(path) as f:
for line in f:
yield line.strip()
# Only reads lines as needed — never loads entire file
for line in read_large_file("10gb_logfile.txt"):
if "ERROR" in line:
print(line)
break # Stops reading immediately
Generator expressions provide a compact syntax:
# List comprehension — eager, stores everything in memory
squares_list = [x**2 for x in range(10_000_000)] # ~80 MB
# Generator expression — deferred, stores almost nothing
squares_gen = (x**2 for x in range(10_000_000)) # ~100 bytes
The generator expression looks almost identical (parentheses instead of brackets) but uses a tiny fraction of the memory.
Lazy Properties
The @property decorator lets you defer attribute computation until first access:
class Report:
def __init__(self, raw_data):
self._raw_data = raw_data
self._summary = None
@property
def summary(self):
if self._summary is None:
self._summary = expensive_analysis(self._raw_data)
return self._summary
Python 3.8 introduced @functools.cached_property, which handles the caching automatically and is even simpler:
from functools import cached_property
class Report:
def __init__(self, raw_data):
self._raw_data = raw_data
@cached_property
def summary(self):
return expensive_analysis(self._raw_data)
The analysis only runs when .summary is first accessed. If no one ever reads the summary, the computation never happens.
Deferred Imports
Heavy libraries like pandas, numpy, or torch can take hundreds of milliseconds to import. Deferring imports until they’re needed speeds up program startup:
def analyze_data(path):
import pandas as pd # Only imported when this function is called
df = pd.read_csv(path)
return df.describe()
Python 3.12 introduced importlib.util.LazyLoader for more structured deferred imports, and PEP 690 proposed full lazy import support at the language level.
itertools: Deferred Pipelines
The itertools module provides composable deferred operations:
import itertools
# Process items in a pipeline — nothing executes until consumption
data = range(1_000_000)
filtered = filter(lambda x: x % 3 == 0, data)
mapped = map(lambda x: x ** 2, filtered)
first_ten = itertools.islice(mapped, 10)
# Only now does computation happen — and only for 10 items
results = list(first_ten)
Each step wraps the previous without doing any work. The actual computation flows through the pipeline only when list() pulls values out.
When Deferred Computation Backfires
- Repeated access — If a generator is consumed in multiple places, you need to either recreate it or convert to a list. Generators are single-use.
- Debugging difficulty — Errors surface when values are consumed, not when they’re defined. A bug in a generator expression might only crash later in the pipeline, making stack traces confusing.
- Unpredictable timing — In real-time systems, you might need guaranteed execution time. Deferred computation introduces variable latency at the point of consumption.
- Database cursors — Lazily iterating over database results holds the connection open. If the consumer is slow, you might hit connection timeouts.
Common Misconception
Some developers think “lazy is always better.” It’s not. If you’re going to consume every item anyway, eager computation with a list can actually be faster because of cache locality and reduced overhead per item. Deferred computation shines when you might not need everything or when the full dataset doesn’t fit in memory.
The one thing to remember: Deferred computation postpones work until results are needed, saving time and memory — but it’s most valuable when you’re processing large datasets partially or want faster startup times, not as a universal replacement for eager evaluation.
See Also
- Python Algorithmic Complexity Understand Algorithmic Complexity through a practical analogy so your Python decisions become faster and clearer.
- Python Async Performance Tuning Making your async Python faster is like organizing a busy restaurant kitchen — it's all about flow.
- Python Benchmark Methodology Why timing Python code once means nothing, and how fair testing works like a science experiment.
- Python C Extension Performance How Python borrows C's speed for the hard parts — like hiring a specialist for the toughest job on the worksite.
- Python Caching Strategies Understand Python caching strategies with a shortcut-road analogy so your app gets faster without taking wrong turns.