Two Phases
A ChirpApphas two distinct phases:
- Setup -- mutable. Register routes, middleware, filters, error handlers.
- Runtime -- frozen. The app compiles its route table, creates the kida environment, and becomes effectively immutable.
from chirp import App
app = App()
# --- Setup phase (mutable) ---
@app.route("/")
def index():
return "Hello"
@app.route("/about")
def about():
return "About"
app.add_middleware(my_middleware)
# --- Freeze happens here ---
app.run() # Compiles routes, freezes config, starts serving
The transition happens when app.run() is called, or on the first ASGI __call__(). After freeze, attempting to register new routes raises an error.
Why Freeze?
Free-threading (Python 3.14t) means multiple threads handle requests concurrently. If the route table, middleware stack, or template environment could be mutated during request handling, you would need locks everywhere.
Instead, Chirp freezes the app once. All shared state becomes immutable. No locks needed for the hot path.
Setup Phase Freeze Runtime Phase
─────────────────────┬──────────────────────────────
@app.route() │ Request handling
app.add_middleware() │ Compile (immutable data)
@app.template_filter │ routes, (no locks on
app.on_startup() │ create env shared state)
─────────────────────┴──────────────────────────────
Lifecycle Hooks
Register callbacks for startup and shutdown:
@app.on_startup
async def connect_db():
app.db = await Database.connect("sqlite:///app.db")
@app.on_shutdown
async def close_db():
await app.db.close()
For per-worker initialization (useful with multi-threaded serving):
@app.on_worker_startup
async def init_worker():
# Runs once per worker thread
pass
@app.on_worker_shutdown
async def cleanup_worker():
# Runs once per worker thread on shutdown
pass
Thread-Safe Freeze
The freeze operation uses double-check locking to be safe under free-threading:
# Simplified -- actual implementation in app.py
if not self._frozen:
with self._freeze_lock:
if not self._frozen:
self._compile_routes()
self._create_kida_env()
self._frozen = True
The first request (or app.run()) triggers the freeze. Concurrent requests block briefly on the lock, then proceed with the frozen state. After that, no synchronization is needed.
Next Steps
- Return Values -- What route handlers can return
- Configuration -- All AppConfig fields
- Routes -- Route registration in detail