Skip to content

Reactive Programming Basics

A little backstory: reactive programming is an evolution of the observer pattern.

The observer pattern provides an intuitive event-driven programming paradigm, treating data (subjects) and observers as fundamental objects. When data changes, observers are notified and update accordingly.

But typical observer patterns require manually binding dependency relationships between subjects and observers. Reactive programming automates dependency tracking.

How this works?

The principle is simple: call stacks record dependencies between calls. If we consider A calling B as A depending on B, then the relative positions in the call stack reveal the dependency relationships.

Once dependencies are tracked, "reactive" means automatic reactions to data changes. If A depends on B and C, then A reacts to B and C; B and C are A's dependencies. That's reactive programming in essence.

Since live editing is invaluable in frontend development, reactive programming has flourished in languages like JavaScript. HMR (this project) brings cutting-edge reactive programming to Python, aiming to be the best-maintained choice balancing flexibility, performance, and accessibility.

Signals and Effects

In HMR, there are two core primitives: Signal and Effect. A Signal acts as a data source:

from reactivity import signal

s = signal(0)  # initial value is 0

print(s.get())  # use .get() to read the signal

s.set(1)  # update its value to 1

print(s.get())

This prints 0 then 1. Without Effects, Signals are just data containers.

Effects subscribe to data sources and rerun when they change:

from reactivity import signal, effect

s = signal(0)

effect(lambda: print(s.get()))  # prints 0 initially

Unlike a simple print(s.get()), when s changes, related Effects automatically rerun. For example:

s.set(1)

This prints 1, even without explicitly calling print(s.get()) again.

In the examples above, we used lambdas as handlers. But you can pass any callable! I personally prefer the decorator style:

@effect
def _():
    print(s.get())

Idempotent Computations

HMR provides another primitive: Derived, representing data processing pipelines. Values are cached if dependencies haven't changed. Computation is lazy (unlike Effects, which are uncached and shouldn't return values—Effects are about "what to do with data" while Derived is about "returning processed data").

from reactivity import signal, derived

s = signal(0)

@derived
def f():
    print("expensive computing ...")
    return s.get() * 2

Unlike Effects that run immediately, nothing happens until you call f():

print(f())
# expensive computing ...
# 0
print(f())
# 0

Multiple calls return cached values without recomputing, since idempotent pipelines should return the same result when inputs are unchanged.

But after s.set(...), calling f() again triggers recomputation:

s.set(1)
print(f())
# expensive computing ...
# 2
print(f())
# 2

Dependency Graph

Signals, Derived, and Effects form the complete reactive primitives. Imagine a graph (like neural network visualizations): leftmost nodes are pure data sources (inputs/files/time), depending on nothing. Rightmost are Effects, depending on nothing else. Middle nodes are intermediate computations, depending on left nodes and depended on by right nodes.

from reactivity import signal, derived, effect

s = signal(0)

@derived
def f():
    return s.get() // 2

@effect
def _():
    print(f())

# output: 0

s.set(1)
# no output because f() = 0, unchanged, no need to rerun effect

s.set(2)
# output: 1

s.set(3)
# no output

HMR's reactivity engine handles all this behind the scenes: ensuring idempotent operations don't rerun unnecessarily, while guaranteeing data changes are reflected without being lost.


Let's take a step further. Obviously, any script can be viewed this way. Local data analysis typically follows:

  1. Load data from files
  2. Process and compute statistics
  3. Present results in some form

Towards Hot Reloading

Even if the reactive paradigm doesn't appeal to you (I'd love to hear why in the repo discussions!), we offer a non-intrusive way to enhance development experience: the hmr CLI.

As mentioned, any script can be seen as a reactive graph (even without explicit usage). The HMR CLI embodies this philosophy. It provides a drop-in replacement for the Python CLI:

  1. HMR treats each file as a data source (signal)—whether Python modules or files opened via open/pathlib
  2. Imported variables are derived values
  3. The entry file you run with python foo.py or python -m foo.bar is an Effect, since nothing depends on it

When files change (code edits or data updates), directly affected Python module reruns. Updated variables trigger dependent modules to rerun, propagating upward until the entry (or stopping earlier, like not every butterfly wing flap causes a hurricane).

Simply use hmr ... instead of python ... to run your code, and see results update instantly when saving files—saving hundreds of times the development time! Compared to your changes, python ... cold starts waste time on unchanged parts.

Although I haven't formally measured it, unless you're developing heavy libraries, your code compilation is typically less than 1‰ of third-party libraries and Python bootstrap time—this should be Python development common sense.

Thanks to meticulous engineering and perfectionism, HMR brings instant feedback to every Python project.

While HMR's original goal was bringing Vite/Vitest-like experiences from JavaScript to Python, it does more. As a more flexible language (trading some performance), Python makes finer-grained runtime dependency tracking possible (via module-level __getattr__/__setattr__, async context isolation, etc.). JavaScript developers, come and try Python!

Note

While the previous section briefly touched on hot reloading implementation, this represents just one application of the reactive programming framework. Hot reloading is an optional feature that can be easily implemented with our reactivity engine, but it's not the sole purpose or requirement of this library. The reactive programming paradigm offers much broader potential beyond just development tooling, enabling sophisticated data flow management, automatic state synchronization, and interactive applications across various domains.

What's Next

For optional configurations of reactive primitives, reaction batching, async reactivity, etc., navigate to signals, derived, effects, and advanced documentation pages.