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 raiseTypeError 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:Error summary
All operations that would silently hang on live streams raiseTypeError instead:
