---
title: "FastAPI lifespan() Locals — Must Access via request.app.state"
tags: [fastapi, lifespan, app-state, python, gotcha]
source: daily/2026-05-14.md
created: 2026-05-15
---

# FastAPI lifespan() Locals — Must Access via `request.app.state`

## Rule

Variables declared inside `lifespan()` are **local to that function**. They are NOT module-level exports. Importing them from `app.main` causes `ImportError` or returns `None`.

## Wrong

```python
# app/main.py
@asynccontextmanager
async def lifespan(app: FastAPI):
    knowledge_base_service = KnowledgeBaseService()
    yield

# routes/some_router.py
from app.main import knowledge_base_service  # ImportError — not exported
```

## Correct

```python
# app/main.py
@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.knowledge_base_service = KnowledgeBaseService()
    yield
    # cleanup if needed

# routes/some_router.py
@router.get("/query")
async def query(request: Request):
    svc = request.app.state.knowledge_base_service
    # or safely:
    svc = getattr(request.app.state, "knowledge_base_service", None)
```

## Why

`lifespan()` is a coroutine, not a module. Its local namespace is destroyed after `yield` returns. The canonical FastAPI pattern is `app.state` — a `State` object attached to the `FastAPI` instance that persists for the application lifetime.

## Diagnosis

The bug typically surfaces as HTTP 500 during the first request, with a `NameError` or `AttributeError` in the traceback. The lifespan itself runs fine (service initialises, logs look healthy), but route handlers can't reach the service.

## See Also

- FastAPI docs: [Lifespan Events](https://fastapi.tiangolo.com/advanced/events/)
- Related pattern in this project: `analysis_routes.py` uses `request.app.state.knowledge_base_service`
