Skip to main content

⌁⌁ Chirp

PyPI version Python 3.14+ License: MIT Status: Alpha

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

bengal_chirp-0.8.2.tar.gz (1.1 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bengal_chirp-0.8.2-py3-none-any.whl (874.1 kB view details)

Uploaded Python 3

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

Hashes for bengal_chirp-0.8.2.tar.gz
Algorithm Hash digest
SHA256 0b2f74cf3293a0ec4ee24c3f6660f31c162b7c279ae90c8b6e43f030d7c1aeb3
MD5 8df9ea23dce7e12c32ed6e0a87fcf84f
BLAKE2b-256 0845461e1ba1b128e5cc41c1242db54da481921cd25803c8d14779b0c8529b6e

See more details on using hashes here.

Provenance

The following attestation bundles were made for bengal_chirp-0.8.2.tar.gz:

Publisher: python-publish.yml on lbliii/chirp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for bengal_chirp-0.8.2-py3-none-any.whl
Algorithm Hash digest
SHA256 eae70b1c3dff7e83568ecad35c2c247a33073e98098330a9e33c0397b1256a95
MD5 43b4ef06bae7d30d40d7bb7e1c014ca4
BLAKE2b-256 ef9f214b43c837e8a3fa8e1ed5002ced71df20ae47615a3a62d94d198733d921

See more details on using hashes here.

Provenance

The following attestation bundles were made for bengal_chirp-0.8.2-py3-none-any.whl:

Publisher: python-publish.yml on lbliii/chirp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page