Skip to main content

robject

Reactive objects for Python. Declare a class the way you would a dataclass, and its attributes become observable. Derived values recompute only when what they read actually changed, side effects re-run automatically, and a burst of writes can be grouped into a single notification.

It's the signals / computed / effect model from modern UI frameworks, packaged as plain Python classes. Pure standard library, no dependencies.

from robject import RObject, computed, effect, batch

class Cart(RObject):
    price: float = 0.0
    qty: int = 1

    @computed
    def total(self) -> float:
        return self.price * self.qty

cart = Cart(price=2.5)
effect(lambda: print("total is", cart.total))   # prints: total is 2.5

with batch():
    cart.price = 3.0
    cart.qty = 4
# prints once: total is 12.0

Features

  • Reactive fields from ordinary class annotations, with defaults, required fields, field(default_factory=...) and inheritance.
  • @computed properties: lazy, cached and read-only. Dependencies are tracked automatically, including dynamic ones (branches) and ones on other objects. Exceptions are cached too, and cycles are detected.
  • Effects re-run whenever anything they read changes. They are glitch-free: an effect never sees a half-updated state.
  • Subscriptions deliver Change(obj, name, old, new) events for fields and computed properties. A computed property notifies only when its value really changes.
  • Batching coalesces writes. Subscribers get one event per attribute (none if the value ends up back where it started), and effects run once.
  • Safety rails: an error in one callback doesn't stop the others (the first one is re-raised afterwards), and runaway feedback loops raise instead of hanging.
  • Typed (py.typed), standard library only, Python 3.10+.

Install

From a checkout (the package is not published to PyPI):

python3 -m venv .venv && . .venv/bin/activate
pip install .

Quickstart

from robject import RObject, Change, batch, computed, effect, field, subscribe

class Todo(RObject):
    title: str                                 # required
    done: bool = False
    tags: list = field(default_factory=list)   # mutable defaults need a factory

class TodoList(RObject):
    items: tuple = ()

    @computed
    def remaining(self) -> int:
        return sum(not t.done for t in self.items)

todos = TodoList(items=(Todo(title="write docs"), Todo(title="ship")))

# Subscribe to specific attributes (or all of them, by passing no names).
unsubscribe = subscribe(todos, lambda ch: print(f"{ch.name}: {ch.old} -> {ch.new}"), "remaining")

todos.items[0].done = True          # prints: remaining: 2 -> 1

# Effects track whatever they read, across objects.
watcher = effect(lambda: print("left:", todos.remaining))   # prints: left: 1

with batch():                       # delivery waits until the block ends
    todos.items[1].done = True      # remaining: 1 -> 0 ...
    todos.items = todos.items + (Todo(title="celebrate"),)   # ... -> 1
# The effect re-runs once and prints: left: 1
# `remaining` ended where it started, so the subscriber prints nothing.

watcher.dispose()
unsubscribe()

Fields are tracked by assignment. Mutating a list in place (todo.tags.append(...)) is invisible, so assign a new value instead (todo.tags = [*todo.tags, "x"]), or use immutable containers such as tuples.

API overview

Everything below is importable from robject.

Name Description
RObject Base class. Annotated class attributes become reactive fields (except ClassVar). Instances are constructed with keyword arguments only. Unknown names and missing required fields raise TypeError. A subclass that defines __init__ must call super().__init__(**kwargs). repr() shows the field values.
field(*, default=..., default_factory=None) Explicit field declaration. You need it for mutable defaults: a bare list/dict/set/bytearray default raises ValueError.
computed Decorator for a lazy, cached, read-only property whose dependencies are tracked. Assigning to it raises AttributeError. Exceptions are cached until a dependency changes, and self-reference raises RuntimeError("cycle detected ...").
effect(fn) -> Effect Runs fn() now and again after any reactive value it read changes. If the first run raises, the effect is disposed and the exception propagates. Also works as a decorator.
Effect Returned by effect. Has .dispose(), .run(), a .runs counter and .disposed. It is also a context manager that disposes on exit.
subscribe(obj, callback, *names) -> unsubscribe Calls callback(Change) when the named fields or computed properties of obj change. With no names it watches all of them. Unknown names raise ValueError. Watched computed properties are re-evaluated eagerly. The returned function cancels the subscription and is idempotent.
Change NamedTuple(obj, name, old, new). For a computed property whose previous evaluation raised, old is None.
batch() Context manager (and decorator) that defers delivery until the outermost batch exits, including on exception. Reads inside a batch already see the new values.
untracked() Context manager. Reads inside it don't become dependencies.
fields(obj_or_cls) -> tuple[str, ...] Field names in declaration order, with base classes first.
snapshot(obj, *, include_computed=False) -> dict Plain dict of the current values. Reading it inside an effect makes the effect depend on every included attribute.
__version__ "0.1.0"

Semantics worth knowing

  • A write that is == to the current value is a no-op.
  • Outside a batch, delivery is synchronous: by the time an assignment returns, subscribers and effects have run.
  • Subscribers and effects may write to reactive values. Their work is processed in further rounds of the same flush. If 100 rounds pass without settling, the flush raises RuntimeError("reactive updates did not settle ...").
  • An effect's own reads are tracked; reads made by callbacks it triggers are not.
  • The reactive graph is process-global and not thread-safe. Use it from one thread, such as a UI or event-loop thread.

Development

python3 -m unittest discover -s tests -v

The tests use only unittest, so python3 -m pytest works too if you have pytest.

License

MIT

Metadata

Release files for robject 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for robject 0.1.0
File Size Uploaded Provenance
robject-0.1.0.tar.gz 18.4 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for robject 0.1.0
File Interpreter ABI Platform
robject-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 32.1 kB

Release files / robject-0.1.0.tar.gz

Download URL robject-0.1.0.tar.gz
Size 18.4 kB
Tags Source
SHA-256 checksum
How to use checksums
ef8774c453c10d759dba96eab45c3163e3b6b58eea125a6e8676877b1b9c2b6e
BLAKE2b-256 checksum
How to use checksums
38db77d3772cbec330962aa9e9356f088f8fb331c6a46008d774fe382d14e22a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release files / robject-0.1.0-py3-none-any.whl

Download URL robject-0.1.0-py3-none-any.whl
Size 13.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ff84c0e12b519c72d2f0f2ddd3a779b42259772604dca4fe88a41745fa0052d0
BLAKE2b-256 checksum
How to use checksums
2302d78306400a0e9335a9a6ac8c192c9c6fca9a203b7bd952e3fa135ed0b3aa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Security for robject 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

0.0.1

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page