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.
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.
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.
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.