Philosophy

The design principles that shape every Chirp decision

Page actions AI-ready formats and sharing
Open LLM text
Share with AI
Ask Claude Ask ChatGPT Ask Gemini Ask Copilot

Chirp's API didn't fall out of one big decision -- it falls out of a handful of consistent instincts about what a web framework should and shouldn't do for you. This page names those instincts so you can judge whether Chirp's bets match yours. If you'd rather see the code, jump to Architecture; if you want the hard lines on what Chirp won't do, see Non-Goals.

Design Principles

These are distilled from building bengal, kida, patitas, and rosettes -- not as rigid rules, but as consistent instincts that shape every decision.

  1. 1

    The obvious thing should be the easy thing

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

    You never make someone understand the system to use the system. The simple call works. The architecture reveals itself only when you need it.

    Five lines to hello world. Return a string, get a response. Return aTemplate, get rendered HTML. Return a Fragment, get a block. The type is the intent.

  2. 2

    Data should be honest about what it is

    If something doesn't change after creation, it shouldn't pretend it might. If something is built incrementally, it should be honest about that too.

    • Requestis frozen. Received data doesn't change.
    • Response is built through .with_*()chains. It is constructed incrementally, then sent.
    • AppConfigis frozen. Configuration doesn't change at runtime.
    • Routeis frozen. The route table doesn't mutate after compile.

    Don't force immutability where the shape of the problem is mutable -- match the tool to the truth.gis mutable because per-request state is mutable.

  3. 3

    Extension should be structural, not ceremonial

    Never make someone inherit from a base class just to participate. If a thing quacks like a middleware, it is a middleware.

    # A function is middleware
    async def timing(request: Request, next: Next) -> Response:
        start = time.monotonic()
        response = await next(request)
        return response.with_header("X-Time", f"{time.monotonic() - start:.3f}")
    
    # A class is middleware
    class RateLimiter:
        async def __call__(self, request: Request, next: Next) -> Response:
            ...
    

    The system discovers capability from shape, not from lineage. The Middlewareprotocol accepts either.

  4. 4

    The system should be transparent

    No proxies hidingtype: ignore. No magic globals. No "it works but don't look at how."

    If someone reads the code, the flow is traceable from entry to exit:

    • Request enters through the ASGI handler
    • Middleware pipeline executes in registration order
    • Router matches the path via trie lookup
    • Handler is called with injected arguments
    • Return value is negotiated into a response
    • Response is sent back through the middleware stack

    No hidden context, no implicit behavior, no action-at-a-distance. The full path -- from trie match to the negotiated response -- is laid out in Architecture.

  5. 5

    Own what matters, delegate what doesn't

    Own the interface, own the developer experience, own the hot path. Delegate the commodity infrastructure.

    • Own: Template integration (kida, same author), routing, middleware protocol, return-value negotiation, fragment rendering
    • Delegate: Async runtime (anyio), form parsing (python-multipart), session signing (itsdangerous), password hashing (argon2)

    Write the template engine because templates are the thing. Use anyio for the async runtime because writing your own is insane.

Non-Goals

Chirp deliberately holds zero per-client server view state, and the bright lines that protect that property are maintained in one canonical place. A few headlines:

  • No stateful ORM. "SQL in, frozen dataclasses out" — database access is your choice.
  • No WebSocket return type. SSE over WebSockets, always.
  • No WSGI, no Python floor below 3.14. The free-threading identity bet.

For the full list — including in-core admin/CRUD, email, background jobs, general rate limiting, and telemetry — and the honest alternative for each, see the canonical Non-Goals doc.

Next Steps