CodeRigor

FREE LESSON

Lazy transformations and consumption budgets

Compose pull-based selection and mapping while measuring exactly how much a one-shot input is consumed.

The existing iterator lesson established iterables, generators and one-pass traversal. Now make consumption part of a composable API. A lazy pipeline runs as its consumer requests values. Returning a generator object is only the beginning: its body must also avoid eagerly materializing the entire source when the first request arrives.

A selection-and-mapping stage should read candidates until one is accepted, transform that one value, yield it, and suspend. A downstream take requests a finite output prefix. Demand travels upstream through the stages, so the consumer controls when the next search begins.

Demand pulls accepted values
start / 3
One downstream next request may inspect several candidates. With odd values rejected, requesting the first output reads 1 and 2, then suspends.
Demand pulls accepted values
One downstream next request may inspect several candidates. With odd values rejected, requesting the first output reads 1 and 2, then suspends.
Python
from itertools import islice

reads = []
def source():
    for value in [1, 2, 3, 4, 5]:
        reads.append(value)
        yield value

selected = (value * 10 for value in source() if value % 2 == 0)
print(reads)
print(tuple(islice(selected, 1)))
print(reads)

Construction reads nothing from the source generator. One accepted output examines candidates 1 and 2. It does not read 3 merely to prove that another candidate exists. This exact budget matters when values are expensive, arrive from another producer, or must remain available to a later consumer of the same iterator.

The build's take returns up to limit items in a tuple. Validate a negative limit before consumption, and let zero request nothing. itertools.islice(source, limit) supplies this stop behavior without a lookahead. A simple loop can accidentally fetch one more item before checking a counter; count at the demand boundary instead.

The unconsumed tail belongs to the iterator
start / 4
take(source, 2) consumes 1 and 2 without lookahead. The next direct request returns 3, and consumed values do not reappear.
The unconsumed tail belongs to the iterator
take(source, 2) consumes 1 and 2 without lookahead. The next direct request returns 3, and consumed values do not reappear.

A generator is a one-shot iterator, not a reusable dataset. After partial consumption, later consumers receive only the remaining suffix. After exhaustion, iterating again produces no values. Calling iter on the generator does not rewind it. Reusable transformation code needs a fresh source supplied on each call, a source factory, or an intentional materialized snapshot.

Python
def doubled(values):
    for value in values:
        yield value * 2

stream = doubled([1, 2])
print(next(stream))
print(tuple(stream))
print(tuple(stream))

Laziness changes error timing. If a transform fails on the third accepted item, construction can succeed and earlier pulls can succeed; the error surfaces only when that item is requested. Do not catch it and return a normal end-of-stream signal unless that recovery is explicitly part of the contract. Exposing the original exception helps the consumer distinguish a failure from valid exhaustion.

A finite output budget is not a finite raw-work budget. A filter may reject a million candidates before producing one output. If it never accepts from an infinite source, even take(..., 1) may never return. Similarly, max or a final sort requires exhaustion and cannot summarize an infinite stream. tee can split consumption but may buffer a large history when consumers separate; it is not a free rewind.

The bug hunts target eager source materialization and one extra consumed item. In your build, record source reads, use a poison suffix that raises if read unnecessarily, retain a partially consumed iterator, and test an empty result. Such deterministic probes are stronger evidence of the consumption contract than timing a small input or asserting that a generator object exists.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Focus on suspension at yield and execution on demand. Trace source reads in the lab instead of timing performance: filtering may examine many inputs for one output, and a generator remains one-shot.