Skip to content

Observable

The core observable class that provides reactive values.

A reactive value that automatically notifies dependents when it changes.

Observable wraps any Python value and makes it reactive. When you modify the value, all computations and functions that depend on it recalculate automatically. Wrap any value in an Observable, and functions that read it during reactive execution will re-run when the value changes.

The mechanism works through dependency tracking. When a reactive function executes, it runs within a ReactiveContext. That context watches which observables get accessed via their .value property. Each access registers the observable as a dependency and adds the context's re-run function as an observer. Later, when you call .set() on the observable, it notifies all observers, causing dependent functions to re-execute with fresh values.

Observable implements magic methods to behave like its underlying value. You can use it in boolean contexts (if observable:), string formatting (f"{observable}"), and equality comparisons (observable == 5) without accessing .value explicitly. That transparency makes observables easy to integrate into existing code—they look and feel like regular values.

The notification system uses batched processing with topological sorting. When multiple observables change, the system collects pending notifications and processes them in dependency order—source observables first, then computed observables, then conditional observables. That ordering ensures that when a conditional observable checks its condition values, those values have already been updated.

Circular dependency detection prevents infinite loops. If a computation tries to modify one of its own dependencies (directly or indirectly), the system raises a RuntimeError. The detection works by checking whether the current reactive context depends on the observable being modified.

Attributes:

Name Type Description
key str

Unique identifier for debugging and serialization

_value

The current wrapped value

_observers Set[Callable]

Set of observer functions to notify on change

Class Attributes

_current_context: Current reactive execution context (None when not in reactive execution) _context_stack: Stack of nested reactive contexts for proper dependency tracking _pending_notifications: Set of observables waiting to notify observers _notification_scheduled: Whether notification processing is scheduled _currently_notifying: Set of observables currently notifying (prevents re-entrant notifications)

Parameters:

Name Type Description Default
key Optional[str]

A unique identifier for this observable (used for debugging and serialization). If None, will be set to "" and updated in set_name when used as a class attribute.

None
initial_value Optional[T]

The initial value to store. Can be any type compatible with the generic type parameter.

None

Raises:

Type Description
RuntimeError

If setting this value would create a circular dependency (e.g., a computed value trying to modify its own input).

Example
from fynx.observable import Observable

# Create an observable
counter = Observable("counter", 0)

# Direct access (transparent behavior)
print(counter.value)  # 0
print(counter == 0)   # True
print(str(counter))   # "0"

# Subscribe to changes
def on_change(new_value):
    print(f"Counter changed to: {new_value}")

counter.subscribe(on_change)
counter.set(5)  # Prints: "Counter changed to: 5"
Note

While you can create Observable instances directly, it's often more convenient to use the observable() descriptor in Store classes for better organization and automatic serialization support.

See Also

Store: For organizing observables into reactive state containers computed: For creating derived values from observables reactive: For creating reactive functions that respond to changes

Initialize an observable value.

Parameters:

Name Type Description Default
key Optional[str]

A unique identifier for this observable (used for serialization). If None, will be set to "" and updated in set_name.

None
initial_value Optional[T]

The initial value to store

None

key

key

Get the unique identifier for this observable.

value

value

Get the current value of this observable.

Accessing this property reads the wrapped value. If called within a reactive context (during execution of a reactive function or computation), it also registers this observable as a dependency. That registration happens automatically—the current ReactiveContext (if any) adds this observable to its dependency set and registers itself as an observer.

The dependency tracking enables automatic re-execution. When you later call .set() on this observable, all registered observers (including reactive contexts that depend on it) get notified and re-run their functions with fresh values.

Returns:

Type Description
Optional[T]

The current value stored in this observable, or None if not set.

Note

This property is tracked by the reactive system. Use it instead of accessing _value directly to ensure proper dependency tracking. Outside reactive contexts, reading .value behaves like a regular property access.

Example
from fynx.observable import Observable
from fynx import reactive

obs = Observable("counter", 5)
print(obs.value)  # 5

# In a reactive context, this creates a dependency
@reactive(obs)
def print_value(val):
    print(f"Value: {val}")

__bool__

__bool__()

Boolean conversion returns whether the value is truthy.

This allows observables to be used directly in boolean contexts (if statements, boolean operations) just like regular values.

Returns:

Type Description
bool

True if the wrapped value is truthy, False otherwise.

Example
obs = Observable("flag", True)
if obs:  # Works like if obs.value
    print("Observable is truthy")

obs.set(0)  # False
if not obs:  # Works like if not obs.value
    print("Observable is falsy")

__eq__

__eq__(other)

Equality comparison with another value or observable.

Compares the wrapped values for equality. If comparing with another Observable, compares their wrapped values.

Parameters:

Name Type Description Default
other object

Value or Observable to compare with

required

Returns:

Type Description
bool

True if the values are equal, False otherwise.

Example
obs1 = Observable("a", 5)
obs2 = Observable("b", 5)
regular_val = 5

obs1 == obs2      # True (both wrap 5)
obs1 == regular_val  # True (observable equals regular value)
obs1 == 10        # False (5 != 10)

__hash__

__hash__()

Hash based on object identity, not value.

Since values may be unhashable (like dicts, lists), observables hash based on their object identity rather than their value.

Returns:

Type Description
int

Hash of the observable's object identity.

Note

This means observables with the same value will not be considered equal for hashing purposes, only identical objects.

Example
obs1 = Observable("a", [1, 2, 3])
obs2 = Observable("b", [1, 2, 3])

# These will have different hashes despite same value
hash(obs1) != hash(obs2)  # True

# But identical objects hash the same
hash(obs1) == hash(obs1)  # True

__repr__

__repr__()

Developer representation showing the observable's key and current value.

Returns:

Type Description
str

A string representation useful for debugging and development.

Example
obs = Observable("counter", 42)
print(repr(obs))  # Observable('counter', 42)

__set_name__

__set_name__(owner, name)

Called when this Observable is assigned to a class attribute.

This method implements the descriptor protocol to enable automatic conversion of Observable instances to appropriate descriptors based on the owning class type.

For Store classes, the conversion is handled by StoreMeta metaclass. For other classes, converts to SubscriptableDescriptor for class-level observable behavior.

Parameters:

Name Type Description Default
owner Type

The class that owns this attribute

required
name str

The name of the attribute being assigned

required
Note

This method is called automatically by Python when an Observable instance is assigned to a class attribute. It modifies the class to use the appropriate descriptor for reactive behavior.

Example
class MyClass:
    obs = Observable("counter", 0)  # __set_name__ called here

# Gets converted to a descriptor automatically
instance = MyClass()
print(instance.obs)  # Uses descriptor

__str__

__str__()

String representation of the wrapped value.

Returns the string representation of the current value, enabling observables to be used seamlessly in string contexts.

Returns:

Type Description
str

String representation of the wrapped value.

Example
obs = Observable("name", "Alice")
print(f"Hello {obs}")  # Prints: "Hello Alice"
# Note: String concatenation with + requires explicit .value access
message = "User: " + str(obs)  # Works with str() conversion

add_observer

add_observer(observer)

Add an observer function that will be called when this observable changes.

Parameters:

Name Type Description Default
observer Callable

A callable that takes no arguments

required

remove_observer

remove_observer(observer)

Remove an observer function.

Parameters:

Name Type Description Default
observer Callable

The observer function to remove

required

set

set(value)

Set the value and notify all observers if the value changed.

This method updates the observable's wrapped value and triggers change notifications to all registered observers. The update only occurs if the new value differs from the current value (using != comparison). If the value hasn't changed, observers are not notified—that avoids unnecessary re-computations.

Before updating, the method checks for circular dependencies. If the current reactive context depends on this observable (directly or indirectly), setting the value would create a cycle. The system raises a RuntimeError in that case, preventing infinite loops.

When the value does change, the observable adds itself to a pending notifications set. The notification system processes these in topological order—source observables first, then computed observables, then conditional observables. That ordering ensures that when a conditional observable checks its condition values, those values have already been updated.

Parameters:

Name Type Description Default
value Optional[T]

The new value to set. Can be any type compatible with the observable's generic type parameter.

required

Raises:

Type Description
RuntimeError

If setting this value would create a circular dependency (e.g., a computed value trying to modify its own input).

Example
from fynx.observable import Observable

obs = Observable("counter", 0)
obs.set(5)  # Triggers observers if value changed

# No change, no notification
obs.set(5)  # Same value, observers not called
Note

Equality is checked using the != operator, so custom objects should implement proper equality comparison if needed.

subscribe

subscribe(func)

Subscribe a function to react to changes in this observable.

The subscribed function will be called whenever the observable's value changes. The function receives the new value as its single argument. This creates a ReactiveContext that wraps the function and registers it as an observer on this observable.

When you call .set() with a new value, the observable notifies all observers. The subscription system ensures that your function runs with the updated value. The function is not called immediately upon subscription—only when the value actually changes.

The subscription creates a reactive context internally. That context tracks dependencies if your function accesses other observables during execution, enabling automatic re-execution when those dependencies change as well.

Parameters:

Name Type Description Default
func Callable

A callable that accepts one argument (the new value). The function will be called whenever the observable's value changes.

required

Returns:

Type Description
Observable[T]

This observable instance for method chaining.

Example
from fynx.observable import Observable

def on_change(new_value):
    print(f"Observable changed to: {new_value}")

obs = Observable("counter", 0)
obs.subscribe(on_change)

obs.set(5)  # Prints: "Observable changed to: 5"
Note

The function is called only when the observable's value changes. It is not called immediately upon subscription.

See Also

unsubscribe: Remove a subscription reactive: Decorator-based subscription with automatic dependency tracking

unsubscribe

unsubscribe(func)

Unsubscribe a function from this observable.

Parameters:

Name Type Description Default
func Callable

The function to unsubscribe from this observable

required