Nitro
A collection of Python web-dev utils inspired by smart solutions across the ecosystem. Use them together or separately, with your favourite framework.
Templating
You just write Python functions. They return HTML. Powered by Datastar for interactivity. Inspired by FastHTML.
from nitro.html.components import * # noqa: F403
from nitro.html import template as templ
from your.components import Sidebar, Navbar
# Shared page template - define headers, footers, body and html kwargs and more
htmlkws = dict(lang="en")
page = page_template(htmlkw=htmlkws, lucide=True)
@templ
def template(content, title: str):
return page(
Fragment(
Sidebar(),
Main(
Navbar(),
Div(
Div(content, id="content"),
cls="p-4 md:p-6 xl:p-12",
),
),
),
title=title,
# wrap_in= HTMLResponse, or your framework's HTML response class
)
from templates import template
# Than use it like this:
@template
def index():
return Div(
H1("Hello, World!"),
P("This is a template."),
)
HTML Generation
You just write Python functions. They return HTML. Powered by Rust for speed. Inspired by FastHTML.
Output
What the browser sees
Jane Doe
Software Engineer
Building things with Nitro. It's just Python. That's the whole pitch.
from rusty_tags import Div, H3, P
from nitro.html.components import Avatar, Badge
def UserCard(user):
return Div(
Div(
Avatar(fallback=user.initials, size="lg"),
Div(
H3(user.name, cls="font-semibold"),
P(user.role, cls="text-sm text-muted-foreground"),
),
cls="flex items-center"
),
P(user.bio, cls="mt-4"),
Div(
*[Badge(tag) for tag in user.tags],
cls="flex gap-2 mt-4"
),
)
# That's it. No JSX. No templates. Just Python.Reactivity
You just add signals. Powered by Datastar. 10kb. Does what React does. No npm install.
from rusty_tags import Div, Button, Span
from rusty_tags.datastar import Signals
def Counter():
sigs = Signals(count=0)
return Div(
Button("-", on_click=sigs.count.sub(1)),
Span(data_text=sigs.count, cls="text-4xl font-bold mx-8"),
Button("+", on_click=sigs.count.add(1)),
data_signals=sigs,
)
# Datastar handles the reactivity.
# No virtual DOM. No hydration. No build step.Live Counter
State lives in HTML. UI updates automatically.
Click the buttons. The count updates. No useState, no Redux, no tears.
Rich Entities
You just define your model. Business logic lives in methods. Persistence is automatic. Built on SQLModel.
Order #1234
Created 2 minutes ago
| Customer | Acme Corp |
| Total | $1,234.00 |
| Items | 3 products |
from nitro.domain.entities.base_entity import Entity
from pydantic import Field
class Order(Entity, table=True):
customer: str = Field(title="Customer")
total: float = Field(default=0.0, title="Total")
status: str = Field(default="draft", title="Status")
def add_item(self, product, qty):
self.total += product.price * qty
self.save() # Just works
def place(self):
if self.total == 0:
raise ValueError("Empty order")
self.status = "placed"
self.save()
# order = Order(customer="Acme Corp")
# order.add_item(widget, 3)
# order.place()
#
# That's your business logic. In one place.Event Routing
You just decorate handlers. Side effects stay decoupled. Built on Blinker with async support and priority ordering.
from nitro.events import event, on, emit
# Create or get a named event
order_placed = event('order-placed')
# Handlers are just decorated functions
@on(order_placed)
def send_confirmation(sender, **kwargs):
EmailService.send(
sender.customer_email,
"Your order is confirmed!"
)
@on(order_placed)
def update_inventory(sender, **kwargs):
for item in sender.items:
Product.get(item.product_id).reduce_stock(item.qty)
@on(order_placed)
def notify_warehouse(sender, **kwargs):
WarehouseQueue.push(sender.id)
# Decoupled. Testable. No spaghetti.Event Flow
What happens when an order is placed
order.place()
Fires 'order-placed' event
Model Views
You just pass your Entity. Forms, tables, cards generated automatically. Field metadata controls everything.
Auto-Generated UI
One Entity, multiple views
class User(Entity)
Define once with Field metadata
from nitro.domain.entities.base_entity import Entity
from nitro.html.components.model_views import (
ModelForm, ModelTable, ModelCard
)
from pydantic import Field
class User(Entity, table=True):
name: str = Field(title="Name")
email: str = Field(
title="Email",
json_schema_extra={'format': 'email'}
)
role: str = Field(
default="member",
json_schema_extra={'icon': 'user'}
)
# Generate a complete form
form = ModelForm(User)
# Generate a data table with sorting
table = ModelTable(User, data=User.all())
# Generate a display card
card = ModelCard(User, instance=user)
# That's < 20 lines for a complete CRUD interface.Credits
Nitro doesn't exist in a vacuum. These projects made it possible.
The idea that HTML can just be Python functions. Revolutionary simplicity.

Datastar
14kb of reactivity. Does what the big frameworks do. No build step required.
Basecoat
Beautiful, accessible UI components. Tailwind CSS powered design system.
SQLModel
Pydantic + SQLAlchemy. The foundation for rich entities with SQL persistence.
Blinker
Simple, elegant signals. Enhanced with async support and priority ordering.
P of EAA
Martin Fowler's patterns book. The blueprints for enterprise architecture.
That's It
Four layers. Pure Python. No magic, just patterns that work.
Use it if it helps. Fork it if you want. File issues if it breaks.
pip install nitro-boost