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.

pythonperformanceoptimizationpatterns

See Also