⌁⌁ Chirp
A Python web framework for HTMX, HTML fragments, streaming HTML, and Server-Sent Events.
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. 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.8.x). See Public API for stable vs provisional exports.
📚 Documentation: lbliii.github.io/chirp
Quick start
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 wiring
The 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.
The core idea
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.
Where to go next
| I want to… | Start here |
|---|---|
| Learn step by step | Learning path · Get Started |
| Understand the architecture | About · Core concepts |
| Build features | Build Apps |
| 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 |
Learn Chirp (examples)
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.
Installation
# pip
pip install bengal-chirp
# uv
uv add bengal-chirp
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.
Reference
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") # -> 302
For htmx-driven form posts that should trigger full-page navigation, prefer hx_redirect()
so both plain browser and htmx requests follow the redirect correctly.
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.
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 mode
Every 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-errors
See Contracts.
DevTools
chirp dev myapp:app
Open 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 |
Production
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 preflight
See the deployment guide.
Benchmarks
Synthetic benchmarks comparing Chirp, FastAPI, Flask, Starlette, and Litestar:
uv sync --extra benchmark
uv run poe benchmark
See benchmarks/README.md for caveats and runners.
Development
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:5173
New 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.
The Bengal Ecosystem
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 | — |
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bengal_chirp-0.8.2.tar.gz.
File metadata
- Download URL: bengal_chirp-0.8.2.tar.gz
- Upload date:
- Size: 1.1 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0b2f74cf3293a0ec4ee24c3f6660f31c162b7c279ae90c8b6e43f030d7c1aeb3
|
|
| MD5 |
8df9ea23dce7e12c32ed6e0a87fcf84f
|
|
| BLAKE2b-256 |
0845461e1ba1b128e5cc41c1242db54da481921cd25803c8d14779b0c8529b6e
|
Provenance
The following attestation bundles were made for bengal_chirp-0.8.2.tar.gz:
Publisher:
python-publish.yml on lbliii/chirp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bengal_chirp-0.8.2.tar.gz -
Subject digest:
0b2f74cf3293a0ec4ee24c3f6660f31c162b7c279ae90c8b6e43f030d7c1aeb3 - Sigstore transparency entry: 1956441511
- Sigstore integration time:
-
Permalink:
lbliii/chirp@3f5f69f4118b8168960458109d95152a0b7af6a7 -
Branch / Tag:
refs/tags/v0.8.2 - Owner: https://github.com/lbliii
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@3f5f69f4118b8168960458109d95152a0b7af6a7 -
Trigger Event:
release
-
Statement type:
File details
Details for the file bengal_chirp-0.8.2-py3-none-any.whl.
File metadata
- Download URL: bengal_chirp-0.8.2-py3-none-any.whl
- Upload date:
- Size: 874.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eae70b1c3dff7e83568ecad35c2c247a33073e98098330a9e33c0397b1256a95
|
|
| MD5 |
43b4ef06bae7d30d40d7bb7e1c014ca4
|
|
| BLAKE2b-256 |
ef9f214b43c837e8a3fa8e1ed5002ced71df20ae47615a3a62d94d198733d921
|
Provenance
The following attestation bundles were made for bengal_chirp-0.8.2-py3-none-any.whl:
Publisher:
python-publish.yml on lbliii/chirp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bengal_chirp-0.8.2-py3-none-any.whl -
Subject digest:
eae70b1c3dff7e83568ecad35c2c247a33073e98098330a9e33c0397b1256a95 - Sigstore transparency entry: 1956441590
- Sigstore integration time:
-
Permalink:
lbliii/chirp@3f5f69f4118b8168960458109d95152a0b7af6a7 -
Branch / Tag:
refs/tags/v0.8.2 - Owner: https://github.com/lbliii
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@3f5f69f4118b8168960458109d95152a0b7af6a7 -
Trigger Event:
release
-
Statement type: