Hot Reload Development ¶
Stario does not include a built-in dev server because watchfiles does a better job.
Recommended: watchfiles ¶
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
pythondirectly (e.g."python main.py").watchfilesrestarts your app by sending a SIGINT signal, and wrapping python withuv run(e.g.watchfiles "uv run python main.py") will not work becauseuv runswallows the SIGINT, preventing proper shutdown and reload.Installing
watchfilesas a dev dependency and usinguv runas shown above is the preferred approach. Alternatively, if you don't have it as a project dependency,uvxworks too - just make sure the inner command is still barepython:
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:
- The client opens a long-lived SSE connection to a
/subscribeendpoint. - On connect, the handler immediately sends a full HTML page via
w.patch()(a "fat morph"). - On every state change, it sends the full page again.
- When you save a file,
watchfilesrestarts the server. The SSE connection drops. - Datastar automatically reconnects (via
retry="always"). - 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 ¶
- Use 1 Worker: In development, set
workers=1inapp.serve(). Multiple workers can cause port-binding conflicts during rapid restarts. - Explicit Host: Use
host="127.0.0.1"to avoid macOS firewall prompts.
