The streaming API provides real-time access to graph execution results as they become available, enabling reactive patterns, progress monitoring, and memory-efficient processing of long-running or infinite graphs.
Instead of waiting for complete graph execution to access results, the stream() function yields results immediately as branches complete (reach leaf nodes). This is particularly useful for:
- Real-time processing - React to results as they arrive
- Progress monitoring - Track execution of long-running graphs
- Memory efficiency - Results are popped from stash as they're yielded
- Infinite loops - Memory-safe execution of cyclic graphs
- Early termination - Stop execution when a condition is met
from hyperway import Graph
from hyperway.tools import factory as f
g = Graph()
g.connect(f.add_10, f.add_20, f.add_30)
start = g.connect()[0].a
g.stepper_prepare(start, 5)
s = g.stepper()
# Stream results as they become available
for result in s.stream():
print(f"Result: {result}")
# Output: Result: 65from hyperway.stepper import stream
g.stepper_prepare(start, 5)
s = g.stepper()
# Functional style - pass stepper to stream()
for result in stream(s):
print(f"Result: {result}")Both styles are functionally equivalent - choose whichever fits your coding style.
Streaming is event-driven - it only yields when results are collected (added to stash), not when nodes are executed.
Results are added to stash when execution reaches:
- Leaf nodes - Nodes with no outgoing connections
- Branch ends - When
stash_ends=True(default)
Key Point: Intermediate node execution does NOT trigger yields.
Graph: A → B → C → D (leaf)
↘ E (leaf)
Execution with stream():
1. Execute A → continues to B (NO YIELD - not a leaf)
2. Execute B → continues to C (NO YIELD - not a leaf)
3. Execute C → branches to D and E (NO YIELD - not a leaf)
4. Execute D → LEAF reached → YIELD result from D
5. Execute E → LEAF reached → YIELD result from E
Stream yielded only twice (at D and E), even though 5 nodes executed.
Critical: Results are removed from stash after yielding via .pop().
This prevents memory buildup in looped/cyclic graphs:
# Graph with loop: A → B → C (leaf)
# ↑___↓
# B loops back to itself
for result in s.stream():
process(result)
# Result removed from stash immediately
# Stash stays small even with infinite loops
# After streaming:
assert len(s.stash) == 0 # Stash is emptyStream results from graphs with multiple branches:
source = as_unit(lambda x: x * 2, name='source')
g.add(source, as_unit(lambda x: x + 10, name='branch_1'))
g.add(source, as_unit(lambda x: x + 20, name='branch_2'))
g.add(source, as_unit(lambda x: x + 30, name='branch_3'))
g.stepper_prepare(source, 5)
s = g.stepper()
# Each branch yields when it completes
for result in s.stream():
print(f"Branch completed: {result}")
# Output (order may vary):
# Branch completed: 20
# Branch completed: 30
# Branch completed: 40Track execution progress for long-running graphs:
processed = 0
for result in s.stream():
processed += 1
print(f"Processed {processed} results...")
# Update progress bar, send metrics, etc.Stop execution when a condition is met:
for result in s.stream():
if result > 50:
print(f"Found target: {result}")
break
# Execution stops, remaining branches may not completeBuild a result list while processing:
results = []
for result in s.stream():
results.append(result)
process(result) # Also process immediately
# Now you have both: immediate processing AND a complete list
print(f"All results: {results}")Stream safely handles infinite loops:
# Create loop: A → B → C (leaf)
# ↑___↓
a = as_unit(lambda x: x + 1, name='A')
b = as_unit(lambda x: x * 2, name='B')
c = as_unit(lambda x: x, name='C') # Leaf
g.add(a, b)
g.add(b, c)
g.add(b, b) # Loop back to B
g.stepper_prepare(a, 1)
s = g.stepper()
# Safe with memory limits
count = 0
for result in s.stream():
print(f"Result {count}: {result}")
count += 1
if count > 100:
break
# Stash stays at 0 - results were popped as yielded
assert len(s.stash) == 0Chain stream processing with generators:
def validate_results(stream):
"""Filter and transform streamed results"""
for result in stream:
if result > 10:
yield result * 2
def save_results(stream):
"""Save validated results"""
for result in stream:
database.save(result)
yield result
# Compose pipeline
validated = validate_results(s.stream())
saved = save_results(validated)
# Execute pipeline
list(saved) # Consumes the entire pipelineFunctional-style streaming function.
Parameters:
stepper(StepperC): A stepper instance to stream fromunwrap(bool): If True (default), extract values usingArgsPack.flat()
Yields:
- Result values as they become available
Example:
from hyperway.stepper import stream
for result in stream(s):
process(result)OOP-style streaming method. Delegates to the standalone stream() function.
Parameters:
unwrap(bool): If True (default), extract values usingArgsPack.flat()
Yields:
- Result values as they become available
Example:
for result in s.stream():
process(result)When unwrap=True (default), uses ArgsPack.flat() which returns:
- Single positional arg → the value itself
- Multiple positional args → tuple of args
- Only kwargs → dict of kwargs
- Args + kwargs → tuple of (args, kwargs)
- Nothing → None
When unwrap=False, yields raw ArgsPack objects:
for akw in s.stream(unwrap=False):
print(f"Args: {akw.args}, Kwargs: {akw.kwargs}")
value = akw.flat() # Manually unwrap if neededThe streaming loop uses an optimized pattern:
ok = 1
while ok:
rows = stepper.step()
if len(stepper.stash) == 0:
continue
# Process results...
ok = len(rows) # Natural loop exit - no explicit break neededThis avoids an unnecessary if not ok: break check on every iteration (~3.6% faster).
- POP behavior - Results removed via
.pop()after yielding - Tuple snapshots -
tuple(stepper.stash.keys())prevents dict size change during iteration - Steady-state memory - Stash size stays constant even in infinite loops
Streaming adds overhead. Don't use it when:
- You need all results at once anyway
- Graph execution is trivial (< 5 nodes)
- You're not processing results until completion
In these cases, use the standard approach:
while s.step():
pass
results = s.get_results()# Execute completely first
while s.step():
pass
# Then access all results
results = s.get_results()
for result in results:
process(result)
# Stash accumulates all results in memoryPros:
- Simpler for small graphs
- All results available together
Cons:
- Can't react during execution
- Memory accumulates
- No progress feedback
- Unsafe for infinite loops
# Process as execution happens
for result in s.stream():
process(result)
# React immediately
# Stash is empty after streamingPros:
- Real-time processing
- Memory-safe (results popped)
- Progress monitoring
- Early termination
- Safe for infinite loops
Cons:
- Results not stored (unless you collect them)
- Order may be non-deterministic
- Slightly more complex
Result order depends on execution path and is not guaranteed:
# Multiple branches may complete in any order
for result in s.stream():
print(result)
# Output order varies: [20, 30, 40] or [30, 20, 40] or ...
# If you need ordering, collect and sort:
results = sorted(list(s.stream()))Wrap streaming in async context:
async def async_stream(stepper):
"""Async wrapper for streaming"""
for result in stepper.stream():
await asyncio.sleep(0) # Yield to event loop
yield result
async def process():
async for result in async_stream(s):
await save_async(result)Errors propagate naturally from the graph:
try:
for result in s.stream():
process(result)
except SomeNodeError as e:
print(f"Graph execution failed: {e}")
# Handle error appropriately- Use streaming for long-running graphs - Get feedback during execution
- Always use unwrap=True - Unless you need direct ArgsPack access
- Collect results if needed - Stream doesn't store them for you
- Add safety limits for loops - Use counters or timeouts
- Handle non-deterministic order - Don't assume result sequence
- Consider memory tradeoffs - Streaming vs collecting all results
- Stepper Documentation - Complete stepper API reference
- Topology Guide - Understanding graph structure
- ArgsPack Flat Method - Result unwrapping details
- Result Access Methods - Alternative result retrieval
Pro Tip: The streaming API combines the power of Python generators with graph execution, giving you fine-grained control over how and when results are processed. Use it when you need reactive patterns or memory efficiency!