Dependency injection¶
Depends (since v0.56.0) lets a route handler declare a per-request
resource — a database connection, an HTTP client, a config view — and have
the framework construct it before the handler runs and tear it down after
the response has been sent.
from blackbull import BlackBull, Depends
app = BlackBull()
async def get_db(): # a "provider": async generator
conn = await pool.acquire()
try:
yield conn # ← injected value
finally:
await pool.release(conn) # ← runs after the response is sent
@app.route(path='/items/{id:int}')
async def get_item(id: int, db=Depends(get_db)):
return await db.fetch_item(id)
Use Depends(provider) as the parameter's default value. Simplified
handlers only — middleware and full (scope, receive, send) handlers are
unchanged, the same rule as every other simplified-model feature.
Provider forms¶
| Provider | Injected value | Cleanup |
|---|---|---|
async def p(): yield v |
v (must yield exactly once) |
code after yield / finally, after the response is sent |
async def p(): return v |
v |
none |
def p(): return v |
v |
none |
Providers take no parameters. A provider that itself declares
Depends (a nested dependency) is a registration-time TypeError —
compose inside the provider body instead (call or close over the other
provider). Sync generator providers are rejected; use an async generator.
Lifetimes¶
| Lifetime | Mechanism |
|---|---|
| per-request | Depends(provider) — fresh value each request |
| per-parameter | Depends(provider, use_cache=False) |
| per-app | @app.on_startup / AppConfig; a provider closes over it |
By default (use_cache=True), two parameters of one handler naming the
same provider share a single instance for that request:
async def audit(a=Depends(get_db), b=Depends(get_db)):
assert a is b # one acquire, one release
There is no app-scoped container: an application-lifetime singleton is an object you create at startup and capture in a provider —
engine: Engine | None = None
@app.on_startup
async def boot():
global engine
engine = await create_engine(dsn)
async def get_conn():
async with engine.connect() as conn: # engine: app-scoped, conn: per-request
yield conn
A runnable version of this pattern — fake pool with visible
acquire/release accounting — ships as
examples/dependency_injection.py.
Ordering and errors¶
- Providers resolve in signature order; cleanup runs LIFO (last provider
up, first down), the
AsyncExitStackdiscipline. - Cleanup runs after the response bytes are sent — a client can hold the full response while the DB connection is already back in the pool. (FastAPI behaves the same way.)
- A handler exception unwinds the stack: every provider's
finallyruns, then the exception routes through error handling as usual (500, or the registerederror_handler). - A provider exception before
yieldaborts the request the same way — the handler never runs.
Zero cost when unused¶
Everything is resolved at registration time: a handler that declares
no Depends parameter compiles to exactly the wrapper it compiled to
before this feature existed — no per-request stack, no empty dependency
loop, no reflection. (FastAPI, by contrast, runs solve_dependencies()
and enters two AsyncExitStacks on every request even with no
dependencies declared.) Handlers that do use Depends pay only for what
they declared.
Depends parameters are not request inputs, so they are excluded from the
generated OpenAPI spec.