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. 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. 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.Responseis 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. 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. 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.
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 does not:
- Include an ORM. Database access is your choice. Chirp serves HTML.
- Include an admin panel. Build it yourself with Chirp's own tools.
- Generate OpenAPI specs. Chirp is an HTML-over-the-wire framework, not a JSON API framework.
- Support WSGI. Chirp is ASGI-only. Synchronous Python is not the future.
- Compete with Django. If you need auth, admin, ORM, email, and background jobs by next Tuesday, use Django. Chirp is for people who want to own their stack.
- Abstract away the web platform. Chirp embraces HTML, CSS, and the browser's native APIs.
Next Steps
- Architecture -- How these principles manifest in code
- Comparison -- How Chirp compares to other frameworks
- Thread Safety -- Free-threading patterns