https://github.com/buiapp/reaktiv
Reactive Signals for Python with first-class async support, inspired by Angular's reactivity model.
https://github.com/buiapp/reaktiv
angular async asyncio dependency python reactivity rx signals state state-management
Last synced: about 1 month ago
JSON representation
Reactive Signals for Python with first-class async support, inspired by Angular's reactivity model.
- Host: GitHub
- URL: https://github.com/buiapp/reaktiv
- Owner: buiapp
- License: mit
- Created: 2025-01-29T23:52:30.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2025-03-12T23:42:28.000Z (about 2 months ago)
- Last Synced: 2025-03-13T00:27:22.371Z (about 2 months ago)
- Topics: angular, async, asyncio, dependency, python, reactivity, rx, signals, state, state-management
- Language: Python
- Homepage:
- Size: 110 KB
- Stars: 53
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- fucking-awesome-angular - reaktiv - Reactive Signals for Python with first-class async support, inspired by Angular's reactivity model. (Table of contents / Third Party Components)
- awesome-angular - reaktiv - Reactive Signals for Python with first-class async support, inspired by Angular's reactivity model. (Table of contents / Third Party Components)
README
# reaktiv  [](https://pypi.org/project/reaktiv/) 
**Reactive Signals for Python** with first-class async support, inspired by Angular's reactivity model.
## Why reaktiv?
If you've worked with modern frontend frameworks like React, Vue, or Angular, you're familiar with the power of reactive state management. The idea is simple but transformative: when data changes, everything that depends on it updates automatically. This is the magic behind dynamic UIs and real-time systems.
But why should Python miss out on the benefits of reactivity? `reaktiv` brings these **reactive programming** advantages to your Python projects:
- **Automatic state propagation:** Change a value once, and all dependent computations update automatically.
- **Efficient updates:** Only the necessary parts are recomputed.
- **Async-friendly:** Seamlessly integrates with Python's `asyncio` for managing real-time data flows.
- **Zero external dependencies:** Lightweight and easy to incorporate into any project.
- **Type-safe:** Fully annotated for clarity and maintainability.### Real-World Use Cases
Reactive programming isn't just a frontend paradigm. In Python, it can simplify complex backend scenarios such as:
- **Real-time data streams:** Stock prices, sensor readings, or live updates.
- **User session management:** Keep track of state changes without heavy manual subscription management.
- **Complex API workflows:** Automatically cascade changes across related computations.By combining these features, `reaktiv` provides a robust foundation for building **reactive, real-time systems** - whether for data streaming, live monitoring, or even Python-powered UI frameworks.
## How it Works
`reaktiv` provides three core primitives:
1. **Signals**: Store a value and notify dependents when it changes.
2. **Computed Signals**: Derive values that automatically update when dependencies change.
3. **Effects**: Run side effects when signals or computed signals change.## Core Concepts
```mermaid
graph LR
A[Signal] -->|Value| B[Computed Signal]
A -->|Change| C[Effect]
B -->|Value| C
B -->|Change| C
C -->|Update| D[External System]
classDef signal fill:#4CAF50,color:white;
classDef computed fill:#2196F3,color:white;
classDef effect fill:#FF9800,color:white;
class A,B signal;
class B computed;
class C effect;
```## Installation
```bash
pip install reaktiv
# or with uv
uv pip install reaktiv
```## Quick Start
### Basic Reactivity
```python
import asyncio
from reaktiv import Signal, Effectasync def main():
name = Signal("Alice")async def greet():
print(f"Hello, {name.get()}!")# Create and schedule effect
# IMPORTANT: Assign the Effect to a variable to ensure it is not garbage collected.
greeter = Effect(greet)
greeter.schedule()name.set("Bob") # Prints: "Hello, Bob!"
await asyncio.sleep(0) # Process effectsasyncio.run(main())
```### Using `update()`
Instead of calling `set(new_value)`, `update()` lets you modify a signal based on its current value.
```python
from reaktiv import Signalcounter = Signal(0)
# Standard way
counter.set(counter.get() + 1)# Using update() for cleaner syntax
counter.update(lambda x: x + 1)print(counter.get()) # 2
```### Computed Values
```python
from reaktiv import Signal, ComputeSignal# Synchronous context example
price = Signal(100)
tax_rate = Signal(0.2)
total = ComputeSignal(lambda: price.get() * (1 + tax_rate.get()))print(total.get()) # 120.0
tax_rate.set(0.25)
print(total.get()) # 125.0
```### Async Effects
```python
import asyncio
from reaktiv import Signal, Effectasync def main():
counter = Signal(0)async def print_counter():
print(f"Counter value is: {counter.get()}")# IMPORTANT: Assign the Effect to a variable to prevent it from being garbage collected.
counter_effect = Effect(print_counter)
counter_effect.schedule()for i in range(1, 4):
await asyncio.sleep(1) # Simulate an asynchronous operation or delay.
counter.set(i)# Wait a bit to allow the last effect to process.
await asyncio.sleep(1)asyncio.run(main())
```---
## Advanced Features
### Using `untracked()`
By default, when you access a signal inside a computed function or an effect, it will subscribe to that signal. However, sometimes you may want to access a signal **without tracking** it as a dependency.
```python
import asyncio
from reaktiv import Signal, Effect, untrackedasync def main():
count = Signal(10)
message = Signal("Hello")async def log_message():
tracked_count = count.get()
untracked_msg = untracked(lambda: message.get()) # Not tracked as a dependency
print(f"Count: {tracked_count}, Message: {untracked_msg}")effect = Effect(log_message)
effect.schedule()count.set(20) # Effect runs (count is tracked)
await asyncio.sleep(1)
message.set("New Message") # Effect does NOT run (message is untracked)await asyncio.sleep(1)
asyncio.run(main())
```---
### Using `on_cleanup()`
Sometimes, you need to clean up resources (e.g., cancel timers, close files, reset state) when an effect re-runs or is disposed.
```python
import asyncio
from reaktiv import Signal, Effectasync def main():
active = Signal(False)async def monitor_status(on_cleanup):
print("Monitoring started")
active.get()def cleanup():
print("Cleaning up before next run or disposal")on_cleanup(cleanup)
effect = Effect(monitor_status)
effect.schedule()await asyncio.sleep(1)
active.set(True) # Cleanup runs before the effect runs againawait asyncio.sleep(1)
effect.dispose() # Cleanup runs before the effect is disposedasyncio.run(main())
# Output:
# Monitoring started
# Cleaning up before next run or disposal
# Monitoring started
# Cleaning up before next run or disposal
```---
## Real-Time Example: Polling System
```python
import asyncio
from reaktiv import Signal, ComputeSignal, Effectasync def main():
candidate_a = Signal(100)
candidate_b = Signal(100)
total_votes = ComputeSignal(lambda: candidate_a.get() + candidate_b.get())
percent_a = ComputeSignal(lambda: (candidate_a.get() / total_votes.get()) * 100)
percent_b = ComputeSignal(lambda: (candidate_b.get() / total_votes.get()) * 100)async def display_results():
print(f"Total: {total_votes.get()} | A: {candidate_a.get()} ({percent_a.get():.1f}%) | B: {candidate_b.get()} ({percent_b.get():.1f}%)")async def check_dominance():
if percent_a.get() > 60:
print("Alert: Candidate A is dominating!")
elif percent_b.get() > 60:
print("Alert: Candidate B is dominating!")# Assign effects to variables to ensure they are retained
display_effect = Effect(display_results)
alert_effect = Effect(check_dominance)
display_effect.schedule()
alert_effect.schedule()for _ in range(3):
await asyncio.sleep(1)
candidate_a.set(candidate_a.get() + 40)
candidate_b.set(candidate_b.get() + 10)
await asyncio.sleep(1)asyncio.run(main())
```**Sample Output:**
```
Total: 200 | A: 100 (50.0%) | B: 100 (50.0%)
Total: 250 | A: 140 (56.0%) | B: 110 (44.0%)
Total: 300 | A: 180 (60.0%) | B: 120 (40.0%)
Total: 350 | A: 220 (62.9%) | B: 130 (37.1%)
Alert: Candidate A is dominating!
```## Examples
You can find example scripts in the `examples` folder to help you get started with using this project.
---
**Inspired by** Angular Signals • **Built for** Python's async-first world • **Made in** Hamburg