Skip to content

Nested Paths

Observant supports accessing deeply nested properties through dot-notation paths with observable_for_path(). This is especially useful when working with complex data structures.

Basic Nested Paths

Use dot notation to access nested properties:

from dataclasses import dataclass
from observant import ObservableProxy

@dataclass
class Address:
    city: str
    country: str

@dataclass
class User:
    name: str
    address: Address

user = User(
    name="Alice",
    address=Address(city="New York", country="USA")
)

proxy = ObservableProxy(user, sync=True)

# Access nested property
city_obs = proxy.observable_for_path("address.city")
print(city_obs.get())  # "New York"

# Update nested property
city_obs.set("Los Angeles")
print(user.address.city)  # "Los Angeles"

Optional Chaining

Use ?. syntax (like JavaScript) to safely access properties that might be None:

from dataclasses import dataclass
from observant import ObservableProxy

@dataclass
class Address:
    city: str
    country: str

@dataclass
class User:
    name: str
    address: Address | None = None

# User without an address
user = User(name="Alice", address=None)
proxy = ObservableProxy(user, sync=True)

# Without optional chaining, this would error
# With optional chaining, it returns None
city_obs = proxy.observable_for_path("address?.city")
print(city_obs.get())  # None

# Setting when path is broken is a safe no-op
city_obs.set("New York")  # Does nothing, no error
print(city_obs.get())  # Still None

Deep Nesting with Optional Chaining

You can chain multiple optional segments for deeply nested structures:

from dataclasses import dataclass
from observant import ObservableProxy

@dataclass
class Location:
    city: str = ""
    country: str = ""

@dataclass
class Habitat:
    name: str = ""
    location: Location | None = None

@dataclass
class Animal:
    name: str
    species: str
    habitat: Habitat | None = None

animal = Animal(
    name="Leo",
    species="Lion",
    habitat=Habitat(
        name="Savanna",
        location=Location(city="Nairobi", country="Kenya")
    )
)

proxy = ObservableProxy(animal, sync=True)

# Two levels deep with optional chaining
city_obs = proxy.observable_for_path("habitat?.location?.city")
print(city_obs.get())  # "Nairobi"

# Update works when path exists
city_obs.set("Mombasa")
print(animal.habitat.location.city)  # "Mombasa"

Reactive Updates

When using optional chaining, the observable automatically updates when parent objects change:

from dataclasses import dataclass
from observant import ObservableProxy

@dataclass
class Address:
    city: str

@dataclass
class User:
    name: str
    address: Address | None = None

user = User(name="Alice", address=None)
proxy = ObservableProxy(user, sync=True)

city_obs = proxy.observable_for_path("address?.city")
print(city_obs.get())  # None

# Subscribe to changes
city_obs.on_change(lambda v: print(f"City changed to: {v}"))

# When the parent is set, the observable updates
user.address = Address(city="Boston")
proxy.observable(object, "address").set(user.address)
# Prints: "City changed to: Boston"

Multiple Paths Same Parent

Multiple observables can share the same nested parent efficiently:

proxy = ObservableProxy(user, sync=True)

# Both share the same internal proxy for "address"
city_obs = proxy.observable_for_path("address.city")
country_obs = proxy.observable_for_path("address.country")

# Changes to one don't affect the other
city_obs.set("Chicago")
print(country_obs.get())  # Still "USA"

Use Cases

UI Data Binding

Perfect for binding UI components to nested model properties:

# In a UI framework
name_field.bind(proxy.observable_for_path("user.profile.display_name"))
avatar_field.bind(proxy.observable_for_path("user.profile?.avatar_url"))

Form Handling

Access nested form data easily:

@dataclass
class ShippingAddress:
    street: str
    city: str
    zip_code: str

@dataclass
class OrderForm:
    customer_name: str
    shipping: ShippingAddress
    billing: ShippingAddress | None = None  # Optional

proxy = ObservableProxy(form, sync=True)

# Required fields
shipping_city = proxy.observable_for_path("shipping.city")

# Optional fields (billing might be None if "same as shipping")
billing_city = proxy.observable_for_path("billing?.city")

Summary

Syntax Behavior
"a.b.c" Required path - errors if any segment is None
"a?.b?.c" Optional path - returns None if any ?. segment is None
"a.b?.c" Mixed - a.b required, c optional

The observable_for_path() method provides a clean, JavaScript-like API for working with nested data structures while maintaining full reactivity and type safety.