Skip to content

Getting Started

This guide will help you get started with Observant, a reactive state management library for Python.

Installation

Install Observant using pip:

pip install observant

Basic Concepts

Before diving into code examples, let's understand the core concepts of Observant.

Observable

An Observable is a wrapper around a value that notifies listeners when the value changes. It's the simplest building block in Observant.

from observant import Observable

# Create an observable with an initial value
name = Observable[str]("Alice")

# Register a callback to be notified when the value changes
name.on_change(lambda value: print(f"Name changed to: {value}"))

# Change the value
name.set("Bob")  # Prints: "Name changed to: Bob"

# Get the current value
current_name = name.get()  # Returns: "Bob"

ObservableList and ObservableDict

Observant also provides observable collections that notify listeners when items are added, removed, or updated.

from observant import ObservableList, ObservableDict

# Observable list
tasks = ObservableList[str](["Task 1"])
tasks.on_change(lambda change: print(f"Tasks changed: {change}"))
tasks.append("Task 2")  # Notifies listeners

# Observable dictionary
settings = ObservableDict[str, str]({"theme": "dark"})
settings.on_change(lambda change: print(f"Settings changed: {change}"))
settings["language"] = "en"  # Notifies listeners

ObservableProxy

The ObservableProxy is the most powerful component in Observant. It wraps an object (typically a dataclass) and provides observable access to its fields.

from dataclasses import dataclass
from observant import ObservableProxy

@dataclass
class User:
    name: str
    age: int
    email: str

# Create a user and wrap it with a proxy
user = User(name="Alice", age=30, email="alice@example.com")
proxy = ObservableProxy(user)

# Get observables for individual fields
name_obs = proxy.observable(str, "name")
age_obs = proxy.observable(int, "age")

# Register change listeners
name_obs.on_change(lambda value: print(f"Name changed to: {value}"))
age_obs.on_change(lambda value: print(f"Age changed to: {value}"))

# Update fields
name_obs.set("Alicia")  # Prints: "Name changed to: Alicia"
age_obs.set(31)         # Prints: "Age changed to: 31"

# Save changes back to the original object
proxy.save_to(user)
print(user.name)  # Prints: "Alicia"
print(user.age)   # Prints: 31

Minimal Example

Here's a complete example showing how to use Observant with a simple form:

from dataclasses import dataclass
from observant import ObservableProxy

@dataclass
class LoginForm:
    username: str
    password: str
    remember_me: bool

# Create a form and proxy
form = LoginForm(username="", password="", remember_me=False)
proxy = ObservableProxy(form)

# Add validation
proxy.add_validator("username", lambda v: "Username required" if not v else None)
proxy.add_validator("password", lambda v: "Password too short" if len(v) < 8 else None)

# Track changes
proxy.observable(str, "username").on_change(lambda v: print(f"Username: {v}"))
proxy.observable(str, "password").on_change(lambda v: print(f"Password: {'*' * len(v)}"))
proxy.observable(bool, "remember_me").on_change(lambda v: print(f"Remember me: {v}"))

# Update fields
proxy.observable(str, "username").set("alice")
proxy.observable(str, "password").set("securepassword")
proxy.observable(bool, "remember_me").set(True)

# Check validation
if proxy.is_valid():
    print("Form is valid!")
    proxy.save_to(form)
else:
    print("Validation errors:", proxy.validation_errors())

Anatomy of a Proxy

The ObservableProxy is the central component of Observant. Here's what it provides:

  1. Field Observables: Access individual fields as observables

    name_obs = proxy.observable(str, "name")
    

  2. Collection Observables: Access lists and dictionaries as observable collections

    tasks_list = proxy.observable_list(str, "tasks")
    settings_dict = proxy.observable_dict((str, str), "settings")
    

  3. Validation: Add validators to fields and check validation state

    proxy.add_validator("email", lambda v: "Invalid email" if "@" not in v else None)
    is_valid = proxy.is_valid()
    errors = proxy.validation_errors()
    

  4. Computed Properties: Define properties that depend on other fields

    proxy.register_computed(
        "full_name",
        lambda: f"{proxy.observable(str, 'first_name').get()} {proxy.observable(str, 'last_name').get()}",
        dependencies=["first_name", "last_name"]
    )
    full_name = proxy.computed(str, "full_name").get()
    

  5. Undo/Redo: Track changes and undo/redo them

    proxy = ObservableProxy(user, undo=True)
    proxy.observable(str, "name").set("Bob")
    proxy.undo("name")  # Reverts to previous value
    

  6. Dirty Tracking: Track unsaved changes

    is_dirty = proxy.is_dirty()
    dirty_fields = proxy.dirty_fields()
    proxy.reset_dirty()
    

  7. Saving and Loading: Save changes back to the model or load from a dictionary

    proxy.save_to(user)
    proxy.load_dict({"name": "Charlie", "age": 25})
    

  8. Nested Paths: Access deeply nested properties with dot notation

    # Required nested path
    city_obs = proxy.observable_for_path("address.city")
    
    # Optional chaining (like JavaScript ?.)
    city_obs = proxy.observable_for_path("address?.city")  # Returns None if address is None
    

Next Steps

Now that you understand the basics, you can explore more advanced features: