stario-logo

Stario: a Joyful Rediscovery of Web Development.

✨ Python web framework that amazes with its simplicity. ✨

Stario takes Datastar and puts it on top of Starlette providing a realtime hypermedia-first development experience. Think about it as a way of blending backend and frontend reactivity together in a seamless workflow. Whether you’re prototyping an idea or scaling a project, Stario makes web development simple and enjoyable (again).

Stario Datastar Starlette

Why Stario Stands Out

Stario simplifies web development by A LOT. We've experienced the benefits of the hypermedia-first approach by Datastar that we're building this framework around it. Here’s what makes it special:

  • Backend-driven Reactivity: Stream HTML updates directly from your Python code to the browser. Build live features like counters or dashboards without full page reloads - Multiplayer apps become a breeze.
  • Frontend-driven Reactivity: Add interactivity directly in your HTML with signals, a lightweight solution to propagate and react to changes across your app.
  • Exploration and Prototyping: Turn ideas into working apps in minutes, perfect for experimenting and iterating. The minimal setup philosophy is at the core of Stario.
  • Solid Foundation: Stario is basically Datastar + Starlette + bunch of good practices and good defaults. Everything else is up to you.

Get Started in Seconds

Installation

# using pip
pip install stario
# or using uv
uv add stario

Quick Start: Three Concepts

Create a main.py that showcases Stario's core capabilities:

import asyncio

from stario import Stario
from stario.datastar import Signal
from stario.html import button, div, h1, input_, p
from stario.toys import toy_page

app = Stario()


# 1. Query: Return a full HTML page
@app.query("/")
async def homepage():
    """A query returns data (in this case, HTML)"""
    return toy_page(
        h1("Welcome to Stario"),
        p("Explore three core concepts below:"),
        # Static section
        div(
            h1("1. Query Routes (Read)"),
            p("This entire page was generated by a @query route"),
        ),
        # Interactive section for command
        div(
            h1("2. Command Routes (Write)"),
            input_(
                {"data-bind": "name"},
                {
                    "id": "name",
                    "type": "text",
                    "placeholder": "Enter your name...",
                },
            ),
            button(
                {"data-on:click": "@post('/greet')"},
                "Greet Me",
            ),
            div({"id": "greet"}),  # Updates appear here
        ),
        # Streaming section
        div(
            h1("3. Streaming Responses (Real-time)"),
            button(
                {"data-on:click": "@get('/counter')"},
                "Start Counter",
            ),
            div({"id": "counter"}),  # Updates stream here
        ),
    )


# 2. Command: Update a specific part of the page
@app.command("/greet")
async def greet(name: Signal[str]):
    """
    A command changes state. 
    Here return HTML will replace the div with id "greet".
    """
    return div({"id": "greet"}, f"Hello, {name}! 👋")


# 3. Query that streams: Yield HTML updates over time
@app.query("/counter")
async def counter():
    """
    Streaming responses are just generator functions!

    Instead of returning once, you yield (multiple times).
    Each yield sends an update to the browser.
    Datastar applies each update automatically.
    """
    interval = 0.01
    elapsed = 0
    while elapsed < 30:
        # Each yield updates the counter div in real-time
        yield div(
            {"id": "counter"},
            f"⏱️ Counter: {elapsed:.2f} seconds",
        )
        await asyncio.sleep(interval)
        elapsed += interval

Run it:

uvicorn main:app
# or using uv directly
uv run uvicorn main:app

Visit http://127.0.0.1:8000/ and try:

  1. The page loads - That's the query route (@app.query)
  2. Click "Greet Me" - That's the command route (@app.command) updating the page
  3. Click "Start Counter" - That's a streaming query yielding updates every second

What You Just Learned

  • Queries fetch data (usually with GET). Returning HTML renders immediately.
  • Commands change state (usually with POST). Return HTML to replace specific UI.
  • Streaming sends updates over time using yield. Perfect for real-time features.

All without JavaScript, complex state management, or refreshing the page.

Ready to Build?

Stario is for Python developers who love to create. Check out the tutorials for introduction to building interactive forms, live dashboards, and more. Join our GitHub repository, star the repo, and start building today!

What will you create with Stario? 🚀
Adam Bobowski