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.

🐱 Live demo — Lucky Cat: a flagship ChirpUI crypto-exchange built entirely on server-owned signals, SSE, Suspense, and OOB swaps — no client framework. (source)

[!NOTE] How this is verified Lucky Cat's headline claims are regression-locked in the example test suite: test_app.py proves the race-safe fill path with test_concurrent_buys_never_500 and test_try_place_order_is_atomic_under_threads; test_feed_determinism.py pins the warmed feed with TestSimFeedGoldenSnapshot; test_auth.py covers public browse, gated routes, signed-in access, and CSRF-protected mutations end to end.

from chirp import App

app = App()

@app.route("/")
def index():
    return "Hello, World!"

app.run()

What is Chirp?

Chirp is a Python web framework built for the modern web platform: browser-native UI, HTML over the wire, streaming responses, and Server-Sent Events. Routes return intent — Page, Fragment, OOB, EventStream, Suspense — and the framework handles content negotiation, layout composition, and htmx awareness automatically. One template with named blocks serves as a full page, a fragment endpoint, an SSE payload, and a Suspense deferred block. No make_response(). No jsonify(). The type is the intent.

@app.route("/search")
async def search(request: Request):
    results = await db.search(request.query.get("q", ""))
    return Page("search.html", "results", results=results)
    # Full page for browsers. Fragment for htmx. Same template, same data.
  • Browser-native UI<dialog>, popover, View Transitions, container queries. Let the browser be the framework.
  • HTML over the wire — Full pages, fragments, streaming HTML, and SSE. Built for htmx.
  • Streaming HTML — Shell first, content fills in as data arrives. No loading spinners.
  • Server-Sent Events — Real-time updates over plain HTTP. No WebSocket upgrade required.
  • MCP tools — Register functions as tools callable by LLMs and MCP clients.

Read the Philosophy for the full picture. See Public API for the stable/provisional import surface.

Use Chirp For

  • HTMX-driven web apps — Server-rendered UI with fragment swaps and progressive enhancement
  • Server-rendered applications — Full pages plus partial updates from the same templates
  • Streaming interfaces — Progressive HTML delivery and token-by-token responses
  • Real-time dashboards — SSE-powered updates without WebSocket complexity
  • Teams avoiding heavy frontend stacks — HTML, CSS, templates, and browser-native features
  • AI-assisted developmentapp.check() contracts name the fix (stable category + concrete message), so apps stay buildable from the public API and contract errors

Installation

# pip
pip install bengal-chirp

# uv
uv add bengal-chirp

Requires Python 3.14+.

Chirp works on its own with plain templates. chirp-ui is an optional companion UI layer, not part of the framework core.


Quick Start

chirp new myapp && cd myapp && python app.py
Function Description
chirp new <name> Scaffold an auth-ready project
chirp new <name> --shell Scaffold with a persistent app shell (topbar + sidebar)
chirp new <name> --sse Scaffold with SSE boilerplate (EventStream, sse_scope)
chirp run <app> Start the dev server from an import string
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: production-posture severity (implies --warnings-as-errors)
chirp routes <app> Print the registered route table
chirp --version Print chirp, kida, pounce, and Python versions
App() Create an application
@app.route(path) Register a route handler
Template(name, **ctx) Render a full template
Template.inline(src, **ctx) Render from string (prototyping)
Page(name, block, **ctx) Auto Fragment or Template based on request
PageComposition(template, fragment_block, ...) Python-first composition with regions
Fragment(name, block, **ctx) Render a named template block
Stream(name, **ctx) Stream HTML progressively
Suspense(name, **ctx) Shell first, OOB swaps for deferred data
EventStream(gen) Server-Sent Events stream
hx_redirect(url) Redirect helper for htmx and full-page requests
app.run() Start the development server

Streaming: Stream vs Suspense vs EventStream

Picking the wrong one is the most common return-type mistake. Use this table:

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 (SEO-friendly progressive render) Post-load updates
Suspense Yes — shell renders, deferred blocks stream as OOB swaps Single chunked HTTP response Dashboards / detail pages 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. If you're hesitating between Suspense and EventStream, ask: is this the initial render or a post-load update?


Features

Feature Description Docs
HTMX Patterns Search, inline edit, infinite scroll, modal, and fragment workflows htmx Patterns →
Comparison When Chirp fits compared with Flask, FastAPI, and Django When to Use Chirp →
Routing Pattern matching, path params, method dispatch Routing →
Filesystem routing Route discovery from pages/ with layouts Filesystem →
Route directory contract _meta.py, _context.py, _actions.py, sections, shell context, and route validation Route Directory →
Route introspection Reserved files, inheritance rules, debug headers, and route explorer Route Contract →
Templates Kida integration, rendering, filters Templates →
Fragments Render named template blocks independently Fragments →
Forms form_or_errors, form macros, validation Forms →
Validation chirp.validation — composable rules (required, email, max_length, …) returning a ValidationResult Forms →
Streaming Progressive HTML rendering via Kida Streaming →
SSE Server-Sent Events for real-time updates SSE →
Middleware CORS, sessions, static files, security headers, custom Middleware →
Contracts Validate htmx attrs, form actions, and route-bearing dialog args Contracts →
Testing Test client, assertions, isolation utilities Testing →
Data Database integration and form validation Data →
Optional UI layer chirp-ui companion components and styles chirp-ui →

📚 Full documentation: lbliii.github.io/chirp


Benchmarks

Chirp now ships a synthetic benchmark suite for comparing Chirp, FastAPI, and Flask across JSON and CPU workloads, plus Chirp-specific fused sync and mixed JSON+SSE scenarios.

uv sync --extra benchmark
uv run poe benchmark

See benchmarks/README.md for how the benchmarks work, their caveats, and the available runners.


Production Deployment

Chirp apps run on Pounce, a production-grade ASGI server with HTTP/2, graceful shutdown, Prometheus metrics, rate limiting, and multi-worker scaling. Use chirp check myapp:app --warnings-as-errors for Chirp hypermedia contracts and pounce check --app myapp:app for server preflight. pounce.toml is Pounce-native today; app.run() and chirp run use AppConfig plus CLI flags. See the deployment guide for details.


Usage

Return Values — Type-driven content negotiation

Route functions return values. The framework handles content negotiation based on the type:

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

No make_response(). No jsonify(). The type is the intent.

For htmx-driven form posts or mutations that should trigger a full-page navigation, prefer hx_redirect() so both plain browser and htmx requests follow the redirect correctly.

Fragments and htmx — Render template blocks independently

Kida can render a named block from a template independently, without rendering the whole page:

{# 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)

Full page request renders everything. htmx request renders just the results_list block. Same template, same data, different scope. No separate "partials" directory.

Forms and validationchirp.validation + ValidationError

chirp.validation is a small, composable rule library. Validators are plain callables ((str) -> str | None); rules compose into a dict; validate() returns a ValidationResult that's truthy iff the form is clean.

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 a 422 with the re-rendered form fragment so htmx swaps the error inline; non-htmx requests get the full page back.

Streaming HTML — Progressive rendering

Kida renders template sections as they complete. The browser receives the shell immediately and content fills in progressively:

@app.route("/dashboard")
async def dashboard(request: Request):
    return Stream("dashboard.html",
        header=site_header(),
        stats=await load_stats(),
        activity=await load_activity(),
    )
Server-Sent Events — Real-time HTML updates

Push Kida-rendered HTML fragments to the browser in real-time:

@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, this enables real-time UI updates with zero client-side JavaScript. The server renders HTML, the browser swaps it in.

Middleware — Composable request/response pipeline

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.

Typed Contracts — Compile-time hypermedia validation

Chirp validates the server-client boundary at startup:

# Prints a contract report and exits non-zero on errors.
app.check()

# Optional strict mode: treat warnings as failures too.
app.check(warnings_as_errors=True)

Every hx-get, hx-post, and action attribute in your templates is checked against the registered route table. Every Fragment and SSE return type is checked against available template blocks. SSE safety checks catch broken sse-connect / sse-swap structures and unsafe inherited target scopes before runtime.

For strict CI:

chirp check myapp:app --warnings-as-errors
Debug DevTools — Browser-side diagnostics for htmx and hypermedia

Run the app in debug mode:

chirp dev myapp:app

Open the app in a browser and press Ctrl+Shift+D for Chirp DevTools. The drawer shows htmx activity, effective hx-* inheritance, render plans, native Chirp EventStream traces, View Transitions, DOM diffs, and Swap Doctor warnings.

Browser-capable agents can discover and export diagnostics with:

window.ChirpHtmxDebug.help()
window.ChirpHtmxDebug.exportRecordsJson()

Development

git clone https://github.com/lbliii/chirp.git
cd chirp
uv sync --group dev
uv run pytest -q --tb=short

Run uv commands from the Chirp repository root. Chirp is developed as a standalone project and resolves development dependencies the same way CI does. If an ancestor directory still has old multi-repo dependency overrides, clone or move Chirp outside that parent before running uv sync.


The Bengal Ecosystem

A structured reactive stack written in pure Python for 3.14t free-threading. Chirp is the 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

Python-native. Free-threading ready. No npm required.


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.1.tar.gz (866.8 kB 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.1-py3-none-any.whl (717.7 kB view details)

Uploaded Python 3

File details

Details for the file bengal_chirp-0.8.1.tar.gz.

File metadata

  • Download URL: bengal_chirp-0.8.1.tar.gz
  • Upload date:
  • Size: 866.8 kB
  • 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.1.tar.gz
Algorithm Hash digest
SHA256 bc3c4da6609cd81737409cd6fe2533762a698b361bc646429e0163feef5c4a51
MD5 9fb095a9e2783c4831f83a71d4d235d4
BLAKE2b-256 75a998d7649931ab4f63f124ba6cf55020a2c1a6a4d411504dae189b82be146f

See more details on using hashes here.

Provenance

The following attestation bundles were made for bengal_chirp-0.8.1.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.1-py3-none-any.whl.

File metadata

  • Download URL: bengal_chirp-0.8.1-py3-none-any.whl
  • Upload date:
  • Size: 717.7 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 15588a758fa816d04b2bb7fef2dda0200482c24683fa182c5d989f536650abe9
MD5 1ec70fb0cd2f90ad0bfbca3d2ef460c8
BLAKE2b-256 188f83c9a9109ded1b5d80140924d4ac7064346105e0d36eb99afa7136e389f5

See more details on using hashes here.

Provenance

The following attestation bundles were made for bengal_chirp-0.8.1-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