Skip to main content
Stream methods fall into three categories: lazy, materializing, and terminal. The distinction matters for live (infinite) streams. is_live() walks the source chain to detect live mode - any stream whose ancestor called .live() returns True. All materializing operations and unsafe terminals check this and raise TypeError immediately rather than silently hanging.

Lazy (streaming)

These return generators - each observation flows through one at a time. Safe with live/infinite streams. No internal buffering between stages. These compose freely. A chain like .after(t).filter(pred).transform(xf).limit(10) pulls lazily - the source only produces what the consumer asks for.

Materializing (collect-then-process)

These must consume the entire upstream before producing output. On a live stream, they raise TypeError immediately. On a backend-backed stream (not a transform), both are pushed down to the backend which handles them on its own data structure (snapshot). The guard only fires when these appear on a transform stream whose upstream is live - detected via is_live().

Rejected patterns (raise TypeError)

Safe equivalents

Terminal (consume the iterator)

Terminals trigger iteration and return a value. They’re the “go” button - nothing executes until a terminal is called. .save(target) is not a terminal - it’s a lazy pass-through that appends each observation to target’s backend as the stream is iterated. Pair it with .drain() (sync) or .drain_thread() (background) to actually run the pipeline.

Choosing the right terminal

Batch query - collect results into memory:
Live ingestion - process forever, constant memory:
One-shot - get a single observation:
Bounded live - collect a fixed number from a live stream:

Error summary

All operations that would silently hang on live streams raise TypeError instead: