Hello world

Your first Stario app: a counter with Datastar client signals and one server route that patches them over SSE. Everything lives in a single main.py so you can see the full request path before realtime fan-out.

Stario is a Python 3.14+ framework for hypermedia web apps — explicit routes, server-rendered HTML, optional Datastar realtime, and built-in telemetry. Not ASGI: run with stario serve or stario watch and a bootstrap callable.

For where symbols live (stario vs stario.routing vs stario.http), see the Glossary.

Prerequisites

You need Python 3.14+ and a project tool. These docs use uv; install Python 3.14 with uv python install 3.14 if needed.

1. Run the example

Clone the stario repo (or copy examples/hello-world into your workspace):

bash
git clone https://github.com/bobowski/stario.git
cd stario/examples/hello-world
uv sync
uv run stario watch main:bootstrap

Open http://127.0.0.1:8000 (set STARIO_PORT if the default is busy). For a one-shot run: uv run stario serve main:bootstrap.

2. Play with it first

Before opening main.py:

  1. Click and + — the count changes in the browser (client signals on $count).

  2. Click Server +1 — Python reads your signals, bumps the count, and pushes the update over SSE with SSE(w).patch_signals.

That split — local reactivity plus a short server round-trip — is the smallest Datastar pattern. Getting started builds on it with Relay, SSE element patches, and POST commands.

3. URLs and assets

Routes and static files are declared as module-level data. AssetManifest fingerprints assets at import time; StaticAssets pays compression and caching cost once in bootstrap.

python
from pathlib import Path
from stario import AssetManifest, StaticAssets, UrlPath
 
ASSETS = AssetManifest(Path(__file__).parent / "static")
HOME = UrlPath("/")
INCREMENT = UrlPath("/increment")

One UrlPath constant per route — used in app.get and in views via .href(). No url_for.

4. Views

Pure functions build HTML as a tree of stario.markup.html callables. data.signals seeds client state; data.on wires clicks; data.text binds display text to a signal; at.get opens a GET to the server.

python
from stario.datastar import at, data
from stario.markup import html as h
 
def home_view(count: int):
    return h.Div(
        data.signals({"count": count}),
        h.Button(data.on("click", "$count--"), "-"),
        h.Div({"id": "count"}, data.text("$count")),
        h.Button(data.on("click", "$count++"), "+"),
        h.Button(data.on("click", at.get(INCREMENT.href())), "Server +1"),
    )

responses.html(w, home_view(...)) sends the tree on first paint. Later, SSE(w).patch_signals(...) updates client state without replacing the whole page.

5. Handlers

Every handler has the same shape: async def handler(c: Context, w: Writer) -> None.

python
import stario.responses as responses
from stario.datastar import SSE, read_signals
 
async def home(c, w):
    responses.html(w, home_view(count=0))
 
async def increment(c, w):
    signals = await read_signals(c.req)
    raw = signals.get("count", 0)
    try:
        n = int(raw)
    except TypeError, ValueError:
        n = 0
    signals["count"] = n + 1
    SSE(w).patch_signals(signals)

read_signals expects a JSON object from Datastar on GET and POST requests that carry signals. Validation is plain Python — see Reading and writing signals for typed reads and error responses.

6. Bootstrap

Composition root: build long-lived objects, register static assets, register routes, then yield for shutdown.

python
from stario import App, Span
 
async def bootstrap(app: App, span: Span):
    span.attr("static_dir", str(ASSETS.directory))
    with span.step("static_assets") as s:
        static = StaticAssets(ASSETS)
        s.attrs(static.stats)
    static.register(app)
 
    app.get(HOME, home)
    app.get(INCREMENT, increment)
    yield

span.attrs and span.step show up in startup telemetry (TTY, JSON, or SQLite) before any request is handled. See Telemetry.

7. Scaffold a real project

Single-file examples teach mechanics. New repos should scaffold app/features/ and app/common/ from day one — see AI-assisted development and Structuring apps. Copy AGENTS.md into the repo root for agent sessions.

bash
uv init --app my-app
cd my-app
uv add "stario>=4,<5"
curl -o AGENTS.md https://stario.dev/AGENTS.md
uv run stario watch app.main:bootstrap