Your first reactive notebook

This page builds a small notebook and inspects the dataflow graph ipyflow constructs as the cells run. The narrative uses run_cell("...") to stand in for executing a notebook cell so that every claim here is checked when the docs are built; in JupyterLab you would type the same code into cells.

Note

The dataflow helpers used below (deps, users, lift, code, timestamp, …) live in the top-level ipyflow package. In a real session, run from ipyflow import deps, users, lift, code, timestamp once at the top of your notebook. Throughout this documentation those names are assumed to already be in scope.

Two names in these examples are documentation helpers, not part of ipyflow’s API: run_cell("...") executes its argument as a notebook cell, and reset_flow() (seen in later pages) starts a fresh kernel so an example can number its cells from 1. In JupyterLab you simply type into cells and restart the kernel instead.

Defining some cells

Start with a couple of ordinary assignments. ipyflow watches each assignment and usage and records an edge whenever one symbol is computed from another.

run_cell("raw = 10")
run_cell("scaled = raw * 2")
run_cell("summary = scaled + 1")

Nothing about the code changed – raw, scaled, and summary are plain Python variables. What ipyflow adds is a graph connecting them.

Who depends on whom

deps returns the immediate upstream dependencies of a symbol, and users returns its immediate downstream dependents:

run_cell("assert deps(summary) == [lift(scaled)]")
run_cell("assert deps(scaled) == [lift(raw)]")
run_cell("assert users(raw) == [lift(scaled)]")

Passing the value of a symbol (scaled) to these helpers works because ipyflow’s tracer intercepts the argument and substitutes the corresponding graph node. lift performs that same value-to-node lookup explicitly, returning the internal Symbol – which is what deps and users compare against and return.

To follow the graph transitively rather than one hop at a time, use rdeps (recursive dependencies) and rusers (recursive dependents):

run_cell("assert set(rdeps(summary)) == {lift(raw), lift(scaled)}")
run_cell("assert set(rusers(raw)) == {lift(scaled), lift(summary)}")

Reconstructing a value

Because ipyflow knows the full provenance of a symbol, it can reconstruct the minimal sequence of code that produces it – a backward program slice. The code helper returns that slice:

run_cell("print(code(summary))")
# Cell 1
raw = 10

# Cell 2
scaled = raw * 2

# Cell 3
summary = scaled + 1

The slice includes only the cells that summary actually depends on. Had there been unrelated cells in between, they would be omitted. The same slice is available per cell via cells(n).slice() (see Program slicing).

When was a symbol last updated

Every symbol carries a Timestamp recording the (cell_num, stmt_num) at which it was last written. cell_num is the 1-indexed execution counter; stmt_num is the 0-indexed statement within that cell:

run_cell("assert timestamp(raw).cell_num == 1")
run_cell("assert timestamp(summary).cell_num == 3")
run_cell("assert repr(timestamp(summary)) == 'Timestamp(cell_num=3, stmt_num=0)'")

Now re-run the first cell with a new value. ipyflow stamps raw with a fresh, later timestamp and – crucially – marks everything downstream as out of date:

run_cell("raw = 99")
# `raw` was just rewritten, so its timestamp is now newer than `summary`'s.
run_cell("assert timestamp(raw).cell_num > timestamp(summary).cell_num")
# `scaled` and `summary` still hold their old values but now have a newer
# upstream dependency, so ipyflow considers them stale/waiting.
run_cell("assert lift(scaled).is_waiting")

What reactive execution adds

In the plain-API view above, ipyflow detected that scaled and summary went stale but did not act on it – that is the default lazy execution mode, which only runs the cell you execute. In JupyterLab, the extension surfaces this staleness visually (orange/purple dependency dots) and, when you opt into reactive mode, automatically re-executes the stale downstream cells so the notebook always reflects a top-to-bottom run.

The next guide, Reactive execution, covers the execution modes, flow direction, and scheduling that govern this behavior. To go deeper on the API used above, see Introspecting the dataflow graph.