Hot Reload Development

Stario does not include a built-in dev server because watchfiles does a better job.

Add watchfiles as a dev dependency and run it with uv run:

uv add --dev watchfiles
uv run watchfiles "python main.py" .

This watches the current directory (.) and restarts your app whenever a file changes.

Caution on the command passed to watchfiles: The command string must invoke python directly (e.g. "python main.py"). watchfiles restarts your app by sending a SIGINT signal, and wrapping python with uv run (e.g. watchfiles "uv run python main.py") will not work because uv run swallows the SIGINT, preventing proper shutdown and reload.

Installing watchfiles as a dev dependency and using uv run as shown above is the preferred approach. Alternatively, if you don't have it as a project dependency, uvx works too - just make sure the inner command is still bare python:

bash uvx --with stario watchfiles "python main.py" .

Refined Dev Script

To exclude static files or specific directories from triggering a restart:

uv run watchfiles \
    --filter python \
    --ignore-paths "static,node_modules" \
    "python main.py" \
    .

Instant Refresh with Fat Morphs

You can get instant page refresh on every code change - without any browser-side reload mechanism - by combining watchfiles with Datastar's SSE reconnect and full-page morphs.

The pattern works like this:

  1. The client opens a long-lived SSE connection to a /subscribe endpoint.
  2. On connect, the handler immediately sends a full HTML page via w.patch() (a "fat morph").
  3. On every state change, it sends the full page again.
  4. When you save a file, watchfiles restarts the server. The SSE connection drops.
  5. Datastar automatically reconnects (via retry="always").
  6. On reconnect, the handler sends the full page again - reflecting your latest code changes.

The result: the browser always shows the latest version of your app within moments of saving, with no manual refresh needed.

Example

Here's the pattern from the built-in Tiles template (uvx stario@latest init → tiles):

async def home(c: Context, w: Writer) -> None:
    user_id = str(uuid.uuid4())[:8]
    c["user_id"] = user_id
    w.html(home_view(user_id))

async def subscribe(c: Context, w: Writer) -> None:
    signals = await c.signals(HomeSignals)
    users.add(signals.user_id)

    # Send the full page immediately on connect
    w.patch(home_view(signals.user_id))

    # Then send it again on every state change
    async for event, user_id in w.alive(relay.subscribe("*")):
        w.patch(home_view(signals.user_id))

    # Cleanup on disconnect
    users.discard(signals.user_id)

The home page initiates the SSE connection with auto-reconnect:

data.init(at.get("/subscribe", retry="always"))

The key insight is that with w.patch(), you can send the entire HTML page - not just a fragment, but the full <html>, <head>, and <body>. Datastar will morph the whole DOM, matching elements by id and swapping only what's changed, including content in <head>. This means your SSE handler can just do w.patch(your_full_page_html) on every connect and update, and the client will always update everything in place. You don't need to hand-wire any client-side refresh logic - just send your whole page as HTML, and it works automatically: save your code, server restarts, client reconnects, and your entire app refreshes instantly, out of the box.

Tips for Fast Restarts

  1. Use 1 Worker: In development, set workers=1 in app.serve(). Multiple workers can cause port-binding conflicts during rapid restarts.
  2. Explicit Host: Use host="127.0.0.1" to avoid macOS firewall prompts.