Changelog¶
This page summarizes notable changes across releases.
0.7.2¶
sample()¶
sample(value, signal, at) returns a Computed holding
value as if signal held at. Nothing else changes: not the signal, and not
any cached value.
frame = Signal(0)
position = computed(lambda f: f * 10)(frame)
held = sample(position, frame, 3) # position at frame 3, on every frame
until = sample(position, frame, computed(lambda f: min(f, 3))(frame)) # live, then held from frame 3
- The result recalculates when
atchanges, or when somethingvaluereads other thansignalchanges. Changing onlysignalleaves it cached. atmay be reactive. It is read with the signal's real value, then substituted.- It works inside computeds, through branches,
untracked(),Bindingand native nodes, and samples can be nested. - Values are calculated as on a first read, so a computed's custom
equalhas no earlier value to keep. WhereSignal.at()would keep a cached value thatequalaccepts,sample()gives the new one. - Reads cost nothing extra in a program that never calls
sample().
Use Signal.at() to run code as if a signal held another value, and
sample() to keep a value from another value of a signal.
Breaking changes¶
-
Breaking: A
Computedcan no longer change a signal while it is being evaluated. Doing so raisesRuntimeError, which is cached like any other error from the function. Previously such a change could leave dependents stuck on an old value for good; for example,with frame.at(f):inside a computed froze everything downstream. The rule covers:- assigning
.valueor atracked_fieldsattribute; Signal.at()with a value other than the current one;update(),invalidate(), item assignment, item deletion and attribute assignment on aSignal;invalidate()on aComputed.
It applies inside
untracked()too, and in a customequalor a plugin'supdatedhook while a computed is refreshing. Assigning the value a signal already holds is still allowed, as are writes from effects and from code outside computeds. Usesample()in place ofSignal.at()inside a computed. - assigning
0.7.1¶
Fixes¶
- A plugin's
updatedhook that raises no longer leaves dependents stale. copy.copy()of atracked_fieldsinstance that keeps its fields in the instance dict now holds the values it was copied with.- A
Computedwithequal=no longer callsequalfor a new result that the built-in rules already count as unchanged, such as a newly createdintequal to the previous one.Signalalready worked this way, as described in What counts as a change.
Runtime improvements¶
tracked_fieldsattributes are now read and written by a compiled descriptor. Reading a field outside a calculation is about three times faster, and assigning one about twice as fast.- Refreshing a computed value no longer copies its function and arguments first, which saves an allocation and reference-count updates on every refresh.
0.7.0¶
Breaking changes¶
-
Breaking: Signified's reactive engine is now a compiled extension written in Rust (
signified._core).Signal,Computed,BindingandEffectkeep their API, and most graphs update several times faster. Behavior changes:- A signal written twice while a change is propagating now notifies both times. Previously the second write could leave a dependent computed stale.
- Subclasses can no longer override
update()orvalue; defining either raisesTypeError. Derive aComputed(for example withsignal.rx.map(fn)) instead of overridingvalue. copy.copy()andcopy.deepcopy()of aSignal,ComputedorBindingraiseTypeError. Deep-copying aComputedpreviously produced an object that never updated. Instances usingtracked_fieldsstill copy and pickle.- Reading or writing a
Signalwhose__init__was never called raisesRuntimeError. - Breaking:
subscribe(),unsubscribe(), andnotify()onSignal,Computed, andBindingare now private. Use anEffectto react to changes. - Breaking: Removed
signified.plugins.pm; useplugin_manager. - Breaking: Removed the migration warnings added in 0.6:
signified.migrationandSIGNIFIED_MIGRATION_WARNINGS. To check code written for 0.5, run the warnings on 0.6 before upgrading; see Migrating to 0.6.
Runtime improvements¶
- Each thread now tracks reads, batches, and effects on its own. Previously two
threads using Signified at once, even with unrelated graphs, could raise
RuntimeError: tracking frames are unbalanced, record a read on one thread as a dependency of a computation on another, or hold back each other's effects. Separate graphs can now run on separate threads, and a graph can be handed from one thread to another. Use one graph from one thread at a time: reading aComputedwhile another thread is computing it raisesRuntimeError.
Packaging¶
- Signified now ships a compiled extension, built with maturin. Wheels cover
CPython 3.12 and later on Linux (x86_64 and aarch64, glibc and musl), macOS
(Intel and Apple silicon), Windows x64, and Pyodide 314 (Python 3.14).
Installing on other platforms builds from source and needs a Rust toolchain.
Under Pyodide, a value more than 250 refreshes deep raises
RecursionError, because the browser's stack is much smaller than a native one. - Plugin hooks run whenever a plugin is registered with
plugin_manager;SIGNIFIED_ENABLE_HOOKSandsignified.plugins.HOOKS_ENABLEDare removed. - Plugins no longer use pluggy, and the
pluginsextra is removed.@hookimpl,plugin_manager.register, andplugin_manager.unregisterwork as before.registernow rejects an@hookimplmethod whose name is not a known hook, and no longer takes anameargument. pluggy options such as@hookimpl(tryfirst=True)and hook wrappers are not supported. - Removed the
ipythonextra. Notebook display works the same without it.
0.6.1¶
Breaking changes¶
- Breaking: Reactive objects no longer overload
!=. Comparisons between reactive objects use identity and return a plainbool, matching==. Replace(x != y)withx.rx.ne(y)for reactive value comparisons, alongside the existingx.rx.eq(y).
Runtime improvements¶
- Added
tracked_fields, a class decorator that makes reassigning an attribute invalidate computations that read it. Reads return the stored value unchanged. It works with slotted and dict-based dataclasses, or named attributes of other classes. SignalandComputedacceptequal=, a custom equality such asnp.array_equal. When it reports a new value equal to the previous one, the previous object is kept and dependents are not invalidated.x.rx.with_equal(equal)sets it on an existing value, including values built by operators,computed, andrx.map, and returns that value.Signalforwards item deletion (del signal[key]) to the wrappedlistordictand notifies observers, likesignal[key] = value.Signal.at()andBinding.at()restore the previous version as well as the previous value on exit. Dependents that were not read inside thewithblock no longer recompute afterwards.other @ reactive,other << reactive, andother >> reactivenow produce reactive values instead of raisingTypeError.- Operators with a NumPy array on the left, such as
array + reactiveorarray < reactive, now return oneComputedinstead of an array ofComputedobjects. Passing a reactive value straight to a ufunc, as innp.sin(reactive), now raisesTypeError; usereactive.rx.map(np.sin).
Type inference¶
Type checkers infer more precise results for reactive operators and helpers, and reject operations the wrapped type does not support.
- Operators report the wrapped type's declared result, with the plain or
reactive value on either side, instead of a union of the operand types.
Built-in numbers follow Python's promotion rules, so
Signal(1) / 2isComputed[float]. Dates and datetimes subtract totimedelta, and adding or subtracting atimedeltakeeps the date type. abs,round,divmod,math.floor,math.ceil,math.trunc, and unary-,+, and~infer the declared result, such asComputed[int]forround(x)andComputed[Decimal]forround(decimal, 2).- Indexing keeps the item type, and slicing a list, tuple, or string with a
reactive slice keeps the container type.
rx.containsandrx.in_reject containers that do not support membership. rx.eq,rx.ne, andrx.as_boolkeep custom and literal result types, sorx.wherecan narrow its result when the condition is known to be true or false.as_rx, andrxhelpers that return the source, such asrx.with_equal, keep the exactSignal,Computed, orBindingtype.- Known gap:
divmod(reactive, plain)loses its type when only the plain value's type defines__rdivmod__. Wrapping it inSignalrestores inference.
0.6.0¶
- Added
Bindingfor switching the source followed by existing calculations. - Added
batch()to group writes before effects run, anduntracked()to read values without adding dependencies. - Breaking: signals now store reactive objects as-is, and computed values
can return them as-is.
unrefreads one reactive object at a time. - Breaking: decorated functions read direct reactive arguments only. Use
deep_unrefinside callbacks to read nested containers, and register custom iterable types to keep resolving their contents. - Change detection now compares exact built-in scalars by value and other objects by identity. Computations and effects skip work when their calculated inputs are unchanged, including after an equal-valued binding source change.
- Computed values save ordinary exceptions until an input changes or they are explicitly invalidated. Failed runs track the inputs they reached and can recover.
- Effects run after affected calculations are marked for refresh. Multiple
effect failures are reported together as an
ExceptionGroup. - Breaking: only
Signalforwards attribute and item writes. Unknown attribute names raiseAttributeError. - Removed
rx.peek(fn); userx.tap(fn).
See Migrating to 0.6 for required code changes and How updates work for the full behavior.
0.5.1¶
Add is_reactive to inspect wrappers without reading their values. Improve
union inference for unref and as_rx; runtime resolution is unchanged.
Add deep_unref.register(Type) for custom containers and retain NumPy shape/dtype.
Repeated references are resolved independently; cycles raise RecursionError.
Resolved key/member collisions raise ValueError instead of silently discarding
entries, and unhashable keys/members raise TypeError.
Legacy traversal of unregistered iterables warns that it will be removed in 0.6.0.
Add opt-in migration diagnostics with migration.enable_warnings() or
SIGNIFIED_MIGRATION_WARNINGS=1, including nested containers and safe cleanup
when warnings are treated as errors. Existing resolution and propagation stay
in place while applications identify affected patterns.
Add rx.tap(fn) for the lazy cached callback helper. rx.peek(fn) remains a
deprecated alias until its removal in 0.6.0. Repeated cached reads do not rerun
the callback; unread intermediate updates are skipped.
See Migrating to 0.6 for changes you can make on 0.5.1 and changes that require upgrading the runtime.
0.5.0¶
Removals¶
The following deprecated APIs were removed. Their replacements are listed below.
Functions
reactive_method— usecomputedinstead.as_signal— useas_rxinstead.
Arguments
Computed(fn, dependencies=...)— useComputed(fn)instead.
Public Methods
Variable.add_name— useVariable.with_nameinstead.Variable.as_bool— useVariable.rx.as_boolinstead.Variable.contains— useVariable.rx.containsinstead.Variable.is_not— useVariable.rx.is_notinstead.Variable.eq— useVariable.rx.eqinstead.Variable.where— useVariable.rx.whereinstead.
Plumbing Methods
Variable.observe— useVariable._observeinstead.Variable.unobserve— useVariable._unobserveinstead.
0.4.0¶
@effect decorator¶
A new @effect decorator mirrors @computed, making it easy to define reactive side effects as plain functions:
Like @computed, arguments are automatically unwrapped, so the function always receives plain Python values. The effect stays active as long as you hold a reference to the returned Effect.
Performance improvements¶
The internals were substantially reworked for speed:
- Observer subscriptions are now stored as intrusive linked lists, reducing allocation overhead for high-fan-out signals.
Computednodes reuse existing dependency links across re-evaluations instead of rebuilding them from scratch.- Invalidation uses a global version counter to short-circuit redundant recomputation in deep graphs.
- Common cases in
computed,effect, and other internal implementation details now have cheaper early-exit paths.
0.3.2¶
Bug fixes¶
Fix bug causing __getattr__ to do infinite loops when the object attempts to be deep copied.
Make __setattr__ slightly more robust.
0.3.1¶
Effect rework¶
Effect now auto-tracks its dependencies the same way Computed does. The constructor signature has changed: instead of taking a reactive source and a callback, it now takes a single zero-argument callable. Every reactive value read via .value or unref() inside that callable is automatically registered as a dependency.
# Before (0.3.0)
e = Effect(s, lambda v: print(v))
# After (0.3.1)
e = Effect(lambda: print(s.value))
Conditional branches are handled correctly — only the signals actually read during the most recent run are tracked, and the dependency set updates automatically when branches change.
unref() now participates in dependency tracking¶
Calling unref() inside a Computed or Effect evaluation now registers each unwrapped reactive as a dependency, the same as reading .value directly. Previously unref() bypassed tracking entirely.
Invalidation API¶
Added invalidate() to all reactive values. For Computed, pass force=True to bypass the dependency-version check and guarantee a full re-evaluation on the next read. Use this when the dependency topology changes outside the reactive graph (for example, replacing an object attribute that points to a signal or computed).
Bug Fixes¶
- Fixed transient computed dependency retention: dependencies are now held strongly during evaluation, preventing them from being dropped by garbage collection before downstream updates complete.
- Fixed exception rollback in
ComputedandEffect: if the user function raises, the previous dependency set and staleness state are preserved so the node retries correctly on the next change.
Breaking Changes
Effectconstructor: the two-argument formEffect(source, fn)is gone. Replace withEffect(lambda: fn(source.value)).NestedValueremoved:from signified import NestedValuewill raiseImportError. UseHasValueorReactiveValue; explicit source chains useBinding.- Internal module renames: the private implementation modules have been reorganised. Any code importing directly from
signified.core,signified.types, orsignified.displaywill break. Use the publicsignifiednamespace instead.
Deprecations
as_signal(val)— useas_rx(val)instead.
0.3.0¶
Dependency Tracking¶
Computed now tracks its dependencies automatically at runtime — no need to declare them upfront. Values are only recomputed when actually read, and if a recomputed value turns out to be unchanged, downstream computeds won't needlessly re-run.
Change Detection¶
Signified is smarter about deciding when a Signal's value has actually changed:
- Functions are compared by identity rather than equality.
- Arrays are compared element-by-element.
- Assigning
NaNto a signal that already holdsNaNis treated as no change.
Mutating a signal's contents (e.g. setting an attribute or index) now also correctly invalidates downstream computeds, and unref/deep_unref align with how dependencies are tracked at runtime.
rx namespace¶
Added Signal.rx namespace which includes several methods:
- map (new) - lazy transform helper for derived values.
- peek (new) - lazy pass-through side-effect helper (observational effects).
- effect (new) - eager side-effect subscription with explicit
dispose(). - len (new)
- is_ (new)
- is_not
- in_ (new)
- contains
- eq
- where
- as_bool
Bug Fixes¶
Fixed a bug in the IPython/Jupyter display integration where display observers could be garbage-collected before they had a chance to update.
Documentation¶
Added an interactive Playground to the docs, powered by Pyodide and CodeMirror, so you can try signified right in your browser.
Deprecations
Deprecated functions:
Computed(..., dependencies=...)— thedependenciesargument is now ignored. Dependencies are automatically inferred from reactive reads during evaluation.@reactive_method(...)— use@computedinstead. Any dependency-name arguments are also ignored.
Deprecated several methods
x.as_bool(...)- Will eventually be removed entirely. Usecomputed(bool)(x)instead.x.contains(...)- Usex.rx.containsinstead.x.eq(...)- Usex.rx.eqinsteadx.where(...)- Usex.rx.whereinsteadx.is_not(...)- Usex.rx.is_notinstead
0.2.7¶
- Significantly improved type inference across a wide range of methods.
- Plugins are now disabled by default — set
SIGNIFIED_ENABLE_HOOKS=1to re-enable them. - Several performance improvements.
- Expanded documentation.
- Migrated CI to
uv.
Breaking Changes
- Plugins are now disabled by default.
OrderedSetandOrderedWeakRefSethave been renamed to_OrderedSetand_OrderedWeakRefSet. These were always internal types.
0.2.6¶
- Fixed packaging so NumPy is truly optional.
- Fixed Python 3.14 compatibility.
- Expanded the CI test matrix to cover newer Python versions.
0.2.5¶
- Improved weak-reference handling in the reactive observer internals.
- Updated core/type integration around weakref sets.
- Minor release metadata updates.
0.2.4¶
- Made IPython an optional dependency.
- Removed the hard NumPy dependency from the base install.
- Updated core/display logic to handle optional imports.
0.2.3¶
- Fixed a runtime typing issue for generic forward references (
ReactiveValue/HasValue). - Added and expanded API documentation pages and updated MkDocs configuration.
- Updated docs deployment workflow.
0.2.2¶
- Refactored the implementation into smaller, focused modules:
core,ops,display,types, andutils. - Removed the old monolithic layout.
- Updated docs and test scaffolding to match.
0.2.1¶
- Fixed
deep_unrefto avoid coercing unknown iterables into lists. - Minor packaging and version metadata adjustments.
0.2.0¶
- Added a plugin system (
signified.plugins) with examples. - Added
deep_unref, memory/performance improvements via__slots__, and typing improvements. - Substantially expanded docs: usage guides, limitations, plugin docs, changelog, and theme updates.
0.1.5¶
- Major expansion of the core reactive implementation and type-inference coverage.
- Improved observe/unobserve and change-detection robustness.
- Release and docs workflow improvements.
0.1.4¶
- Repackaged the project into a
src/signified/layout. - Ensured
py.typedis included in the package. - Minor README and metadata cleanup.
0.1.3¶
- Improved README and docs landing content.
- Fixed MkDocs configuration issues.
- Minor source and metadata tweaks.
0.1.2¶
- Added support for Python 3.9+.
- Added broad tests for signals, computed values, reactive methods, and type inference.
- Added CI test workflow and moved to a
src/layout.
0.1.1¶
- Fixed package naming and docs after the project rename.
- Updated project URLs and version metadata.
- Updated changelog entries for the rename transition.
0.1.0¶
- Initial tagged release as
signified.