Getting started with Stario

Build one small app, run it locally, then add a long-lived stream only if you need live updates.

You need Python 3.14+ and a project tool. The steps below use uv.

1. Create a project

Pick one path:

  • Minimal project — an empty app plus Stario:

bash
uv init --app hello-stario
cd hello-stario
uv add stario
  • Scaffolded templatestario init or uvx stario@latest init gives a stock layout and a runnable demo (see Realtime tiles, especially Create the project). Use this when you want batteries included.

Both yield a normal Python project with Stario as a dependency.

2. First pass: static HTML

Replace main.py with:

python
from stario import (
    App,
    Context,
    Span,
    Writer,
    html as h,
    responses,
)
 
 
async def home(c: Context, w: Writer) -> None:
    # c: request-side. w: response-side.
    responses.html(
        w,
        h.HtmlDocument(
            h.Head(
                h.Title("Hello from Stario"),
            ),
            h.Body(
                h.H1("Hello from Stario"),
                h.P("This is a normal HTML page."),
            ),
        ),
    )
 
 
async def bootstrap(app: App, span: Span) -> None:
    # This is the startup span. Add metadata to it.
    span.attr("app.name", "hello-stario")
    span.attr("app.version", "0.1.0")
 
    app.get("/", home, name="home")

Shape of the app:

  • bootstrap(app, span) registers routes and shared setup.

  • span carries startup telemetry.

  • app.get(...) binds a path to a handler.

  • home(c, w) runs per request.

  • responses.html(...) completes an HTML response.

3. Run the app

bash
uv run stario watch main:bootstrap

Open http://127.0.0.1:8000 (default bind). You should see “Hello from Stario”.

4. Second pass: add realtime

When the same page should receive server-driven updates without navigation, Stario uses Datastar for a thin client and SSE from normal handlers.

Update main.py:

python
import asyncio
 
from stario import (
    App,
    Context,
    Span,
    Writer,
    datastar as ds,
    html as h,
    responses,
)
 
 
async def home(c: Context, w: Writer) -> None:
    responses.html(
        w,
        h.HtmlDocument(
            h.Head(
                h.Title("Hello from Stario"),
                ds.ModuleScript(),
            ),
            h.Body(
                # Start a long-lived connection to /ticks.
                ds.init(ds.get(c.app.url_for("ticks"), retry="always")),
                h.H1("Hello from Stario"),
                h.P("This page now receives updates from the server."),
                h.P({"id": "clock"}, "Waiting for ticks..."),
            ),
        ),
    )
 
 
async def tick_stream():
    # Yield one new value every 0.5s.
    tick = 0
    while True:
        tick += 1
        yield tick
        await asyncio.sleep(0.5)
 
 
async def ticks(c: Context, w: Writer) -> None:
    # All handlers can keep the connection as long as they want.
    c.span.event("stream.opened")
 
    async for tick in w.alive(tick_stream()):
        ds.sse.patch_elements(w, h.P({"id": "clock"}, f"Tick {tick}"))
 
    c.span.event("stream.closed")
 
 
async def bootstrap(app: App, span: Span) -> None:
    # This is the startup span. Add metadata to it.
    span.attr("app.name", "hello-stario")
    span.attr("app.version", "0.1.0")
 
    app.get("/", home, name="home")
    app.get("/ticks", ticks, name="ticks")

Reload the page. The paragraph with id clock should update about every 0.5s.

When you add POST actions that send Datastar signal JSON, validate that payload in your own helpers (how-to); the tick stream above is GET-only and does not need that step.

What changed:

  • ds.ModuleScript() loads Datastar in the page.

  • ds.init(ds.get(...)) opens a long-lived GET to /ticks.

  • ticks is still async def handler(c, w); it stays open until the client or server ends the connection.

  • w.alive(...) ties handler lifetime to the connection and shutdown.

  • c.span.event(...) records stream open/close in telemetry.

5. Telemetry in the model

Operations matter as much as code: failures, latency, and deployment context should be observable without ad hoc logging everywhere. The tick handler above already records c.span.event on the stream—you are using the same primitives at a larger scale.

  • Startup and shutdown produce spans with server metadata.

  • span in bootstrap is the startup span; c.span is per request.

  • Use attr / attrs for stable dimensions (version, environment).

  • Use event for discrete steps (stream opened, cache miss, …).

See Telemetry and Telemetry design.