A full-stack Python hypermedia framework with a built-in contract compiler.
Routes return intent — Page, Fragment, EventStream, Suspense, and friends — and Chirp
handles content negotiation, layout composition, and htmx awareness. Install as
bengal-chirp, import as chirp. Requires Python 3.14+.
Chirp ships routing, templates, forms, validation, sessions, auth, streaming HTML, SSE,
static files, security middleware, testing tools, and hypermedia contract checks in one
framework. At startup it compiles routes, typed return declarations, template blocks,
and registries into one immutable internal application model used by chirp check,
runtime transition traces, and testing tools. JSON routes and explicit Response
objects are supported when you need them.
Database access uses Shapes
and an optional in-tree PostgreSQL driver. Background jobs, admin UIs, and email delivery
integrate at the seams — see Non-goals.
Status: alpha (0.9.x). See Public API for stable vs provisional exports. Public positioning and performance language is governed by the machine-checked claims ledger.
📚 Documentation: lbliii.github.io/chirp
pip install 'bengal-chirp[ui]' # [ui] optional but recommended for new projects
chirp new myapp && cd myapp
python app.py # http://127.0.0.1:8000
chirp check myapp:app # validate hypermedia wiringThe scaffold includes routes, templates, and (with [ui]) ChirpUI layouts. No npm, no build step.
Minimal example (no scaffold)
from chirp import App
app = App()
@app.route("/")
def index():
return "Hello, World!"
app.run()For the smallest complete htmx loop (Page, Fragment, forms, tests), follow First Fragment App.
One template, many access patterns. The return type expresses intent; Chirp negotiates the response:
from chirp import App, Page, Request
app = App()
@app.route("/search")
async def search(request: Request):
results = await db.search(request.query.get("q", ""))
return Page("search.html", "results", results=results)
# Browser navigation → full page. htmx request → just the "results" block.No make_response(). No separate partials directory. The type is the intent.
Read Philosophy and Return values for the full model.
The same declarations form Chirp's contract compiler input. chirp check
diagnoses broken route/template/target relationships before a browser reaches
them, while DevTools and transition tests correlate runtime requests back to
the compiled model. The primary output is still a live ASGI application;
chirp freeze is an optional static projection for compatible routes.
See Hypermedia Application Compiler for the architecture and the tested Full-Application Journey for the database, mutation, validation, boosted-navigation, SSE, diagnostics, and optional-export proof.
| I want to… | Start here |
|---|---|
| Learn step by step | Learning path · Get Started |
| Understand the architecture | About · Core concepts |
| Build features | Build Apps |
| Prove a complete database-backed app | Full-Application Journey |
| Run runnable examples | Examples index |
| Compare to other stacks | When to use Chirp |
| See what's intentionally out of scope | Non-goals |
| Look up exports and stability | Public API · Reference · Glossary |
| Contracts, tests, deployment | Quality & Operations |
Follow the learning path on the docs site. Examples are tiered on purpose. Do them in order.
| Tier | Example | You will learn |
|---|---|---|
| 1 — Basics | standalone/hello, standalone/contacts |
Routes, forms, Page / Fragment, validation |
| 2 — App shell | chirpui/contacts_shell |
ChirpUI shell, _actions.py, _context.py, boosted nav |
| 3 — Capstone | chirpui/lucky_cat |
Signals, Suspense, SSE, OOB, secure stack |
Capstone demo — Lucky Cat (tier 3, not the on-ramp)
Live: luckycat-production.up.railway.app ·
Source: examples/chirpui/lucky_cat/
A simulated trading-floor UI built on server-owned signals, SSE, Suspense, and OOB swaps — no client framework. Complete tiers 1–2 first.
Most day-to-day apps use a small set: App, @app.route, Template, Page, forms,
ValidationError, and chirp check. Streaming, signals, and filesystem routing are the next
layer — the tiered examples introduce them in order.
# pip
pip install bengal-chirp
# uv
uv add bengal-chirpThe packaged chirp command uses the direct milo-cli 0.4.x dependency for
typed, lazy command registration. Existing Chirp argv and exit behavior remain
covered by a black-box compatibility suite. The read-only check, diff, and
routes inspections share structured results across CLI, programmatic calls,
MCP, and llms.txt; server, scaffold, freeze, migration, and other write-capable
commands remain human-only.
Optional extras
| Extra | Adds |
|---|---|
[ui] |
chirp-ui components and themes (chirp new emits ChirpUI layouts) |
[forms] |
Multipart form parsing |
[sessions] |
Signed cookie sessions |
[auth] |
Argon2 password hashing |
[passkeys] |
WebAuthn / passkeys |
[ai] |
LLM streaming (httpx) |
[data-pg] |
PostgreSQL via in-tree driver (no extra deps) |
[testing] |
httpx test client transport |
[redis] |
Redis-backed sessions and rate limiting |
[markdown] |
Patitas + Rosettes markdown rendering |
[config] |
python-dotenv for .env loading |
[all] / [full] |
Common optional features bundled |
pip install 'bengal-chirp[ui]'
# or: uv add 'bengal-chirp[ui]'When chirp-ui is installed, chirp check verifies that chirpui-* classes resolve to backing styles.
CLI
| Command | Description |
|---|---|
chirp new <name> |
Scaffold an auth-ready project |
chirp new <name> --shell |
Scaffold with a persistent app shell (topbar + sidebar) |
chirp new <name> --stream |
Simulated token streaming (TemplateStream + EventStream) |
chirp new <name> --sse |
Scaffold with SSE boilerplate (EventStream, sse_scope) |
chirp new <name> --ai |
Scaffold AI chat with tools, SSE activity feed, and secure stack |
chirp run <app> |
Start the dev server from an import string |
chirp dev <app> |
Dev server with Chirp DevTools |
chirp check <app> |
Validate hypermedia contracts |
chirp check <app> --warnings-as-errors |
Fail CI on contract warnings |
chirp check <app> --coverage |
Show contract coverage counters |
chirp check <app> --deploy |
Deploy preflight (implies --warnings-as-errors) |
chirp routes <app> |
Print the registered route table |
chirp --version |
Print chirp, kida, pounce, and Python versions |
Return types — type-driven content negotiation
return "Hello" # -> 200, text/html
return {"users": [...]} # -> 200, application/json
return Template("page.html", title="Home") # -> 200, rendered via Kida
return Page("search.html", "results", items=x) # -> Fragment or Template (auto)
return Fragment("page.html", "results", items=x) # -> 200, rendered block
return Stream("dashboard.html", **async_ctx) # -> 200, streamed HTML
return Suspense("dashboard.html", stats=...) # -> shell + OOB swaps
return EventStream(generator()) # -> SSE stream
return hx_redirect("/dashboard") # -> Location + HX-Redirect
return Response(body=b"...", status=201) # -> explicit control
return Redirect("/login") # -> 302For htmx-driven form posts that should trigger full-page navigation, prefer hx_redirect()
so both plain browser and htmx requests follow the redirect correctly.
Experimental HTTP QUERY — safe body-bearing searches
Chirp supports RFC 10008 QUERY on explicit ASGI routes for controlled
early-adopter use. Choose it only when a read-only query is too large or
structured for a practical URI; ordinary bookmarkable searches and native HTML
forms should stay GET.
The route declares accepted request media types, while the handler keeps using Chirp's normal typed HTML returns and one-template/named-block render surface. Browser, Pounce, Uvicorn, and Nginx proof exists, but stable promotion and universal proxy/CDN support are not claimed. Keep a GET fallback and verify the exact deployment path.
See the HTTP QUERY adoption guide for request failures, CORS, redirects, conditional responses, explicit cache opt-in, compatibility evidence, and the remaining release gates.
Stream vs Suspense vs EventStream
Picking the wrong one is the most common return-type mistake:
| Type | Shell first? | Transport | Use for | Not for |
|---|---|---|---|---|
Stream |
No — flush blocks as they complete | Single chunked HTTP response | Slow first-byte pages with independent sections | Post-load updates |
Suspense |
Yes — shell renders, deferred blocks stream as OOB swaps | Single chunked HTTP response | Dashboards with multiple slow data sources, one round trip | Post-load updates |
EventStream |
N/A — pure event channel | SSE (text/event-stream, long-lived) |
Notifications, tickers, chat tails after the page loads | Initial page render |
Rule of thumb: initial render that streams → Suspense (or Stream for SEO-heavy sections);
updates after the page loads → EventStream for page-local regions, signal() for cross-page
chrome. Multi-target mutations → OOB / FormAction.
See the realtime decision tree.
Fragments and htmx
{# templates/search.html #}
{% extends "base.html" %}
{% block content %}
<input type="search" hx-get="/search" hx-target="#results" name="q">
{% block results_list %}
<div id="results">
{% for item in results %}
<div class="result">{{ item.title }}</div>
{% end %}
</div>
{% endblock %}
{% endblock %}@app.route("/search")
async def search(request: Request):
results = await db.search(request.query.get("q", ""))
if request.is_fragment:
return Fragment("search.html", "results_list", results=results)
return Template("search.html", results=results)Forms and validation
from chirp import Page, ValidationError
from chirp.validation import validate, required, email, max_length
@app.route("/contacts", methods=["POST"])
async def create_contact(request: Request):
form = await request.form()
result = validate(form, {
"name": [required, max_length(200)],
"email": [required, email],
})
if not result:
return ValidationError("contacts.html", "form", errors=result.errors, form=form)
contacts.append(Contact(**result.data))
return Page("contacts.html", "list", contacts=contacts)ValidationError returns 422 with the re-rendered form fragment for htmx; non-htmx requests get
the full page back.
Server-Sent Events
@app.route("/notifications")
async def notifications(request: Request):
async def stream():
async for event in notification_bus.subscribe(request.user):
yield Fragment("components/notification.html", event=event)
return EventStream(stream())Combined with htmx's SSE support, the server renders HTML and the browser swaps it in.
The managed htmx 4 preview uses native hx-sse:connect: rendered Fragment
updates are unnamed HTML frames, and Fragment(target="feed") becomes an
unnamed <hx-partial hx-target="#feed"> update. Named SSEEvents remain DOM
events. Htmx 2 keeps its existing sse-connect / sse-swap channels.
Middleware
No base class. No inheritance. A middleware is anything that matches the protocol:
async def timing(request: Request, next: Next) -> Response:
start = time.monotonic()
response = await next(request)
elapsed = time.monotonic() - start
return response.with_header("X-Time", f"{elapsed:.3f}")
app.add_middleware(timing)Built-in middleware: CORS, StaticFiles, HTMLInject, Sessions, SecurityHeaders, CSRF, Auth, and more. See Request pipeline.
Contracts — static hypermedia validation
app.check() # report and exit non-zero on errors
app.check(warnings_as_errors=True) # strict modeEvery hx-get, hx-post, and action attribute in templates is checked against the route table.
Every Fragment and SSE return type is checked against available template blocks.
chirp check myapp:app --warnings-as-errorsSee Contracts.
DevTools
chirp dev myapp:appOpen the app in a browser and press Ctrl+Shift+D for Chirp DevTools — htmx activity, effective
hx-* inheritance, render plans, EventStream traces, View Transitions, and Swap Doctor warnings.
window.ChirpHtmxDebug.help()
window.ChirpHtmxDebug.exportRecordsJson()Features index
| Topic | Docs |
|---|---|
| HTMX patterns | htmx Patterns |
| Routing & filesystem layout | Pages & navigation |
| Templates & fragments | HTML fragments |
| Forms & data | Forms & validation |
| Streaming & SSE | Streaming updates |
| Middleware | Request pipeline |
| Contracts & debugging | Quality |
| Testing | Testing |
| Optional UI layer | chirp-ui |
Chirp apps run on Pounce, a production-grade ASGI server with HTTP/2, graceful shutdown, Prometheus metrics, rate limiting, and multi-worker scaling.
chirp check myapp:app --warnings-as-errors # hypermedia contracts
pounce check --app myapp:app # server preflightSee the deployment guide.
Benchmarks
Synthetic benchmarks comparing Chirp, FastHTML, FastAPI, Flask, Starlette, and Litestar:
uv sync --extra benchmark
uv run poe benchmarkSee the committed baseline and full artifact for current results, caveats, environment metadata, and runners.
git clone https://github.com/lbliii/chirp.git
cd chirp
make install # once per worktree (.python-version → 3.14t, docs deps)
make test
make site-serve # docs site at http://127.0.0.1:5173New Conductor worktree? Same flow: make install then make site-serve
(or cd site && ./bengal s). Python pin and free-threading env live in git
(.python-version, config/python.env, site/bengal); only .venv is recreated.
Run commands from the repository root. If an ancestor directory has old multi-repo dependency
overrides, clone Chirp outside that parent before running make install.
Docs-site details: site/AGENTS.md.
Python-native stack for 3.14t free-threading. Chirp is the web framework; packages like
chirp-ui sit on top as optional companions.
| ᓚᘏᗢ | Bengal | Static site generator | Docs |
| ∿∿ | Purr | Content runtime | — |
| ⌁⌁ | Chirp | Web framework ← You are here | Docs |
| ʘ | chirp-ui | Optional companion UI layer | — |
| =^..^= | Pounce | ASGI server | Docs |
| )彡 | Kida | Template engine | Docs |
| ฅᨐฅ | Patitas | Markdown parser | Docs |
| ⌾⌾⌾ | Rosettes | Syntax highlighter | Docs |
| ⚡ | Zoomies | QUIC / HTTP/3 | — |
MIT