Skip to main content

Python reactive state management library inspired by MobX

Project description

FynX

FynX Logo

Quick Start Read the Docs Examples Support

PyPI Version Build Status License: MIT Documentation Python Versions

FynX ("Finks") = Functional Yielding Observable Networks

FynX makes state management in Python feel inevitable rather than effortful. Inspired by MobX and functional reactive programming, the library turns your data reactive with minimal ceremony—declare relationships once, and updates cascade automatically through your entire application.

Whether you're building real-time Streamlit dashboards, data pipelines, or interactive applications, FynX ensures that when one value changes, everything depending on it updates instantly. No stale state. No forgotten dependencies. No manual synchronization.

Define relationships once. Updates flow by necessity.

Quick Start

pip install fynx
from fynx import Store, observable

class CartStore(Store):
    item_count = observable(1)
    price_per_item = observable(10.0)

# Reactive computation
total_price = (CartStore.item_count | CartStore.price_per_item) >> (lambda count, price: count * price)
total_price.subscribe(lambda total: print(f"Cart Total: ${total:.2f}"))

# Automatic updates
CartStore.item_count = 3          # Cart Total: $30.00
CartStore.price_per_item = 12.50  # Cart Total: $37.50

This example captures the core promise: declare what should be true, and FynX ensures it remains true. For complete tutorials and patterns, see the full documentation or explore examples/.

Where FynX Applies

FynX works wherever values change over time and other computations depend on those changes. The reactive model scales naturally across domains:

  • Streamlit dashboards with interdependent widgets (see example)
  • Data pipelines where downstream computations recalculate when upstream data arrives
  • Analytics systems visualizing live, streaming data
  • Form validation with complex interdependencies between fields
  • Real-time applications where manual state coordination becomes unwieldy
  • ETL processes with dynamic transformation chains
  • Monitoring systems reacting to threshold crossings and composite conditions
  • Configuration systems where derived settings update when base parameters change

The common thread: data flows through transformations, and multiple parts of your system need to stay synchronized. FynX handles the tedious work of tracking dependencies and triggering updates. You focus on what relationships should hold; the library ensures they do.

This breadth isn't accidental. The universal properties underlying FynX apply to any scenario involving time-varying values and compositional transformations—which describes a surprisingly large fraction of software.

The Mathematical Guarantee

Here's what makes FynX different: the reactive behavior doesn't just work for the examples you see—it works by mathematical necessity for any reactive program you could construct.

FynX satisfies specific universal properties from category theory. These aren't abstractions for their own sake; they're implementation principles that guarantee correctness:

  • Functoriality: Any function you lift with >> preserves composition exactly. Chain transformations freely—the order of operations is guaranteed.
  • Products: Combining observables with | creates proper categorical products. No matter how you nest combinations, the structure remains coherent.
  • Pullbacks: Filtering with & constructs mathematical pullbacks. Stack conditions in any order—the semantics stay consistent.

You don't need to understand category theory to use FynX. The mathematics works beneath the surface, ensuring that complex reactive systems composed from simple parts behave predictably under all transformations. Write declarative code describing relationships, and the universal properties guarantee those relationships hold.

Think of it as a particularly thorough test suite—one that covers not just the cases you thought to write, but every possible case that could theoretically exist (but yes, we’ve got the “real deal” tests if you want to live dangerously).

Observables

Observables form the foundation—reactive values that notify dependents automatically when they change. Create them standalone or organize them into Stores:

from fynx import observable, Store

# Standalone observable
counter = observable(0)
counter.set(1)  # Triggers reactive updates

# Store-based observables (recommended for organization)
class AppState(Store):
    username = observable("")
    is_logged_in = observable(False)

AppState.username = "off-by-some"  # Normal assignment, reactive behavior

Stores provide structure for related state and enable features like store-level reactions and serialization. With observables established, you compose them using FynX's four fundamental operators.

The Four Reactive Operators

FynX provides four composable operators that form a complete algebra for reactive programming:

Operator Operation Purpose Example
>> Transform Apply functions to values price >> (lambda p: f"${p:.2f}")
| Combine Merge observables into tuples (first | last) >> join
& Filter Gate based on conditions file & valid & ~processing
~ Negate Invert boolean conditions ~is_loading

Each operation creates a new observable. Chain them to build sophisticated reactive systems from simple parts. These operators correspond to precise mathematical structures—functors, products, pullbacks—that guarantee correct behavior under composition.

Transforming Data with >>

The >> operator transforms observables through functions. Chain multiple transformations to build derived observables:

from fynx import computed

# Inline transformations
result = (counter
    >> (lambda x: x * 2)
    >> (lambda x: x + 10)
    >> (lambda x: f"Result: {x}"))

# Reusable transformations
doubled = computed(lambda x: x * 2, counter)

Each transformation creates a new observable that recalculates when its source changes. This chaining works predictably because >> implements functorial mapping—structure preservation under transformation.

Combining Observables with |

Use | to combine multiple observables into reactive tuples:

class User(Store):
    first_name = observable("John")
    last_name = observable("Doe")

# Combine and transform
full_name = (User.first_name | User.last_name) >> (lambda f, l: f"{f} {l}")

When any combined observable changes, downstream values recalculate automatically. This operator constructs categorical products, ensuring combination remains symmetric and associative regardless of nesting.

Note: The | operator will transition to @ in a future release to support logical OR operations.

Filtering with & and ~

The & operator filters observables to emit only when conditions are met. Use ~ to negate:

uploaded_file = observable(None)
is_processing = observable(False)

# Conditional observables
is_valid = uploaded_file >> (lambda f: f is not None)
preview_ready = uploaded_file & is_valid & (~is_processing)

The preview_ready observable emits only when all conditions align—file exists, it's valid, and processing is inactive. This filtering emerges from pullback constructions, guaranteeing consistent semantics no matter how you stack conditions.

Reacting to Changes

React to observable changes using the @reactive decorator, subscriptions, or the @watch pattern:

from fynx import reactive, watch

# Dedicated reaction functions
@reactive(observable)
def handle_change(value):
    print(f"Changed: {value}")

# Inline reactions
observable.subscribe(lambda x: print(f"New value: {x}"))

# Conditional reactions
condition1 = observable(True)
condition2 = observable(False)

@watch(condition1 & condition2)
def on_conditions_met():
    print("All conditions satisfied!")

Choose the pattern that fits your context. These reactions fire automatically because the dependency graph tracks relationships through the categorical structure underlying observables.

Additional Examples

Explore the examples/ directory for demonstrations across use cases:

File Description
basics.py Core concepts: observables, subscriptions, computed properties, stores, reactive decorators, conditional logic
cart_checkout.py Shopping cart with reactive total calculation
advanced_user_profile.py Complex reactive system with validation, notifications, persistence, and sophisticated computed properties
streamlit/store.py Custom StreamlitStore with automatic session state synchronization
streamlit/todo_app.py Complete reactive todo list with Streamlit UI, real-time updates, and automatic persistence
streamlit/todo_store.py Todo store with computed properties, filtering, and bulk operations

These examples demonstrate how FynX's composable primitives scale from simple to sophisticated. The consistency across scales follows from the mathematical foundations.

The Mathematical Foundation

Time-varying values have structure. When you create an Observable<T> in FynX, you're working with something that behaves like a continuous function from time to values—formally, $\mathcal{T} \to T$ where $\mathcal{T}$ represents the temporal domain. Observables possess deeper mathematical character: they form what category theorists call an endofunctor $\mathcal{O}: \mathbf{Type} \to \mathbf{Type}$ on Python's type system.

Functors preserve transformations. If you have a function $f: A \to B$ that transforms regular values, a functor lifts that transformation to work on structured values. The >> operator implements this lifting—it takes ordinary functions and makes them work on observables.

# Regular function on values
def double(x): return x * 2
def add_ten(x): return x + 10

value = 5
result = add_ten(double(value))  # 20

# Same composition, lifted to observables
obs = observable(5)
obs_result = obs >> double >> add_ten  # Observable(20)

Functors must satisfy two laws that guarantee predictable behavior:

$$\mathcal{O}(\mathrm{id}) = \mathrm{id} \qquad \mathcal{O}(g \circ f) = \mathcal{O}g \circ \mathcal{O}f$$

The first law: doing nothing to an observable still does nothing. The second: composing two functions then lifting is identical to lifting each separately and composing.

# Identity law: O(id) = id
obs = observable(42)
assert (obs >> (lambda x: x)).value == obs.value

# Composition law: O(g ∘ f) = O(g) ∘ O(f)
composed = obs >> (lambda x: add_ten(double(x)))
chained = obs >> double >> add_ten
assert composed.value == chained.value  # Both are 94

These laws mean that no matter how you chain transformations with >>, the order of operations preserves exactly as you'd expect from ordinary function composition.

When you combine observables with |, you're constructing a Cartesian product in the observable category: $\mathcal{O}(A) \times \mathcal{O}(B) \cong \mathcal{O}(A \times B)$. This isomorphism is elegant—combining two separate time-varying values produces a single time-varying tuple, and these perspectives are equivalent.

first_name = observable("Jane")
last_name = observable("Doe")

# Product creates a tuple observable
full_name = (first_name | last_name) >> (lambda f, l: f"{f} {l}")

first_name.set("John")  # full_name automatically becomes "John Doe"

The product structure ensures that combining observables remains symmetric and associative regardless of nesting order.

a = observable(1)
b = observable(2)
c = observable(3)

# Associativity: (a | b) | c ≅ a | (b | c)
left_assoc = (a | b) | c   # ((1, 2), 3)
right_assoc = a | (b | c)  # (1, (2, 3))

# Both represent the same product structure, just different tuple nesting
# The mathematical product A×B×C is unique up to isomorphism

Filtering introduces pullbacks. When you use & with a predicate $p: A \to \mathbb{B}$, FynX constructs a universal way of selecting subobjects. The predicate maps values to the boolean domain $\mathbb{B}$, pulling back along the "true" morphism:

$$ \mathcal{O}(A) \xrightarrow{\mathcal{O}(p)} \mathcal{O}(\mathbb{B}) \xrightarrow{\text{true}} \mathbb{B} $$

data = observable(42)
is_positive = data >> (lambda x: x > 0)
is_even = data >> (lambda x: x % 2 == 0)

# Pullback: only emits when both conditions hold
filtered = data & is_positive & is_even

data.set(42)   # filtered emits: 42 (positive and even)
data.set(-4)   # filtered doesn't emit (not positive)
data.set(7)    # filtered doesn't emit (not even)

Pullbacks guarantee that combining filters with & behaves associatively and commutatively. Stack conditions in any order—the semantics remain consistent because they derive from a universal construction.

# Commutativity: a & b ≡ b & a
filter1 = data & is_positive & is_even
filter2 = data & is_even & is_positive

# Both represent the same pullback
data.set(42)
assert filter1.value == filter2.value  # Both emit 42

The categorical perspective provides proofs, not just patterns. When you chain operations in FynX, you're not hoping the library handles edge cases correctly—the mathematics guarantees it must.

# Complex composition: all laws hold automatically
price = observable(100.0)
quantity = observable(3)
discount = observable(0.1)
is_valid = quantity >> (lambda q: q > 0)

# Functor laws + product structure + pullback semantics = correct composition
total = ((price | quantity) >> (lambda p, q: p * q)) & is_valid
discounted = total >> (lambda t: t * (1 - discount.value))

quantity.set(5)  # Everything updates correctly by mathematical necessity

Functoriality ensures that structure-preserving transformations in your domain remain structure-preserving when lifted to observables. Product and pullback constructions come with universal properties that dictate precisely how composition behaves. No special cases. No hidden gotchas.

Changes propagate through your system correctly because the underlying category theory proves they must. FynX tracks dependencies and manages updates automatically, but that automation isn't heuristic—it follows necessarily from the categorical structure. You write declarative code describing relationships, and the functor laws, product isomorphisms, and pullback universality ensure those relationships maintain under all transformations.

This is the power of building on mathematical foundations. The theory isn't ornamentation—it's why you can compose observables fearlessly. Category theory gives FynX its correctness guarantees, turning reactive programming from a collection of patterns into a rigorous calculus with laws you can depend on.

Design Philosophy

Deep mathematics should enable simpler code, not complicate it. FynX grounds itself in category theory precisely because those abstractions—functors, products, pullbacks—capture the essence of composition without the accidents of implementation. Users benefit from mathematical rigor whether they recognize the theory or not.

The interface reflects this. Observables feel like ordinary values—read them, write them, pass them around. Reactivity works behind the scenes, tracking dependencies through categorical structure without requiring explicit wiring. Method chaining flows naturally: observable(42).subscribe(print) reads as plain description, not ceremony. The >> operator transforms, | combines, & filters—each produces new observables ready for further composition. Complex reactive systems emerge from simple, reusable pieces.

FynX offers multiple APIs because different contexts call for different styles. Use decorators when conciseness matters, direct calls when you need explicit control, context managers when reactions should be scoped. The library adapts to your preferred way of working.

The library remains framework agnostic by design. FynX has zero dependencies in its core and integrates cleanly with Streamlit, FastAPI, Flask, or any Python environment. Whether you're building web applications, data pipelines, or desktop software, the reactive primitives fit naturally without forcing architectural changes.

One current limitation: FynX operates single-threaded. Async support is planned as the concurrency model matures.

Test Coverage

FynX maintains comprehensive test coverage tracked through Codecov:

Sunburst Diagram Grid Diagram Icicle Diagram
Sunburst Coverage Diagram
Inner circle represents the entire project, radiating outward through folders and files. Size and color indicate statement count and coverage.
Grid Coverage Diagram
Each block represents a file. Size and color indicate statement count and coverage.
Icicle Coverage Diagram
Top section represents the entire project, with folders and files below. Size and color indicate statement count and coverage.

Contributing

Contributions to FynX are welcome. This project uses Poetry for dependency management and pytest for testing.

To learn more about the vision for version 1.0, see the 1.0 Product Specification.

Getting Started

poetry install --with dev --with test
poetry run pre-commit install
poetry run pytest

Pre-commit hooks run automatically on each commit, checking code formatting and style. Run them manually across all files with poetry run pre-commit run --all-files.

Development Workflow

  • Test your changes: poetry run pytest --cov=fynx
  • Check linting: ./scripts/lint.sh
  • Auto-fix formatting: ./scripts/lint.sh --fix
  • Fork and create feature branch: feature/amazing-feature
  • Add tests and ensure they pass
  • Submit PR with clear description of changes

🌟 Love FynX?

Support the evolution of reactive programming by starring the repository



FynX — Functional Yielding Observable Networks

LicenseContributingCode of Conduct

Crafted with ❤️ by Cassidy Bridges

© 2025 Cassidy Bridges • MIT Licensed



Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fynx-0.0.7.tar.gz (59.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fynx-0.0.7-py3-none-any.whl (62.6 kB view details)

Uploaded Python 3

File details

Details for the file fynx-0.0.7.tar.gz.

File metadata

  • Download URL: fynx-0.0.7.tar.gz
  • Upload date:
  • Size: 59.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.2.0 CPython/3.13.7 Linux/6.15.11-2-MANJARO

File hashes

Hashes for fynx-0.0.7.tar.gz
Algorithm Hash digest
SHA256 6b1420c47327aca2861bd6c8a0a18cd5ae2bc06515783de4d7f3ca4e4929436b
MD5 395e9def61ee816a44da20f7584c4c6a
BLAKE2b-256 161398b6a5fd4d2f9c24c10464d89d3dc94c7a0999e8b327a64eab919e3e475d

See more details on using hashes here.

File details

Details for the file fynx-0.0.7-py3-none-any.whl.

File metadata

  • Download URL: fynx-0.0.7-py3-none-any.whl
  • Upload date:
  • Size: 62.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.2.0 CPython/3.13.7 Linux/6.15.11-2-MANJARO

File hashes

Hashes for fynx-0.0.7-py3-none-any.whl
Algorithm Hash digest
SHA256 650c19d8dfc513b4136214c2871600e3878bd8bfafa03f161d91e8438d1ab92d
MD5 0e8e1542260444bcaf4517db869b3af1
BLAKE2b-256 ba07e88f51526f5e379406d15c957942aecaaca0bfa72fa467d924015b973bde

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page