Watchers
Where computed state derives values, watchers let you react to changes — updating a UI widget, writing to disk, sending a network request.
from observ import reactive, watch
state = reactive({"count": 0})
watcher = watch(
lambda: state["count"],
lambda new, old: print(f"count changed from {old} to {new}"),
)
watch(fn, callback) evaluates fn once immediately to collect its dependencies, then calls callback whenever the result of fn changes.
Keep the watcher alive
watch() returns a Watcher object. Hold on to it: when the watcher is garbage collected the callback stops firing. Storing it in a dict or on the object that owns the reaction (self.watchers["count"] = watch(...)) is a common pattern.
Callback signatures
The callback may accept zero, one, or two arguments — observ automatically detects which:
watch(fn, lambda: print("changed!"))
watch(fn, lambda new: print(f"now: {new}"))
watch(fn, lambda new, old: print(f"was: {old}, now: {new}"))
What can be watched
The first argument to watch() is typically a function, but it can also be a proxy (or even a list of proxies) to watch directly, which implies deep=True:
Async functions are supported as well, both as the watched expression and as the callback; coroutines are scheduled on the running asyncio event loop.
Options
immediate
By default the callback only fires on the first change. Pass immediate=True to also call it right away with the initial value (with None for the old value):
This is handy for UI code: the same callback initializes the widget and keeps it up to date.
deep
When watching a function, only the state that is actually read is tracked, and the callback fires when the result changes. With deep=True, the result is also traversed completely, so the callback fires on changes nested anywhere inside a returned container:
state = reactive({"items": [{"done": False}]})
watch(lambda: state["items"], callback) # fires when the list itself changes
watch(lambda: state["items"], callback, deep=True) # also fires when an item's "done" flips
Note
With deep=True (and also when the watched value is a container), the callback's new and old arguments will be the same object, since the watcher can't keep a snapshot of the old state.
sync
By default, callbacks are not run at the moment the state changes: the watcher is queued on the scheduler, which batches and deduplicates updates and runs them on your event loop. Pass sync=True to skip the scheduler and run the callback synchronously on every change. Use this sparingly — for tests, scripts without an event loop, or when you really need the callback to have run before the next line of code.
watch_effect
If there is no meaningful "result" to watch and you just want to run a piece of code whenever any state it touches changes, use watch_effect():
from observ import reactive, watch_effect
state = reactive({"count": 0})
def persist():
save_to_disk(state["count"])
watcher = watch_effect(persist)
The function runs once immediately to collect dependencies, and re-runs (via the scheduler) whenever they change. It is equivalent to watch(fn, deep=True) without a callback.
Watcher lifecycle
The returned Watcher object gives you full control over the reaction:
watcher = watch(lambda: state["count"], callback)
watcher.pause() # dependency changes no longer trigger the callback
watcher.resume() # if anything changed while paused, triggers once now
watcher.stop() # permanently stop and release resources
pause()/resume()— temporarily suspend the watcher. If a dependency changed while paused, the watcher triggers once upon resume. Check the state with thepausedproperty.stop()— permanently deactivate the watcher and release its resources. Calling the watcher object itself (watcher()) is equivalent, which is convenient when a framework expects a teardown callable. Check the state with theactiveproperty.- Deleting the last reference to a watcher deactivates it as well.
Methods as watched functions or callbacks
When you pass a bound method as the watched function or as the callback, observ stores it with a weak reference to the instance. The watcher will then not keep your object alive:
class Display:
def __init__(self, state):
self.watcher = watch(lambda: state["count"], self.update)
def update(self, new):
...
Here the Display instance can be garbage collected normally, even though its watcher references self.update.