Async
Async resolution
The synchronous API stays exactly as it is. Async support is a separate
path you opt into when a dependency is genuinely asynchronous — an
async def factory, or a resource that must be opened and
closed around its lifetime. It uses the standard library's
contextlib.AsyncExitStack, so still no runtime dependencies.
Async factories
Register a coroutine-function factory and resolve it through the async API. The result is cached per its lifetime just like a sync factory:
async def load_settings() -> Settings:
return await read_config()
container.add_singleton_factory(Settings, load_settings)
async def main() -> None:
settings = await container.aresolve(Settings)
The sync resolve() raises
AsyncResolutionRequiredException if the graph needs async
work, so you never silently get back an un-awaited coroutine:
container.resolve(Settings) # AsyncResolutionRequiredException
Async "infects" upward: a plain synchronous class that depends on an async factory is also resolved through the async path.
Async resources
A resource has a lifetime: a database session, a connection pool, an HTTP client. Declare it as an async generator factory:
from typing import AsyncIterator
async def db_session(pool: Pool) -> AsyncIterator[Session]:
session = await pool.acquire()
try:
yield session
finally:
await session.close()
container.add_scoped_factory(Session, db_session)
When the resource is finalized depends on its lifetime:
- scoped / transient — finalized when its
AsyncScopeexits; - singleton — lives until
await container.aclose()at shutdown.
Finalization is LIFO, like nested async with blocks.
Async scopes
Open a scope with async with. Resources resolved inside are
closed when the block exits:
async def handle_request() -> None:
async with container.ascope() as scope:
service = await scope.aresolve(RegisterUser)
await service.execute(...)
# scoped resources finalized here, LIFO
aresolve() on the container (outside a scope) refuses to
open a scoped/transient resource it would have to finalize immediately;
resolve those inside async with container.ascope() instead.
FastAPI
Build the container once in lifespan, open one async scope per request, and close singleton resources on shutdown:
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.container = build_container()
yield
await app.state.container.aclose()
async def request_scope(request: Request) -> AsyncIterator[AsyncScope]:
async with request.app.state.container.ascope() as scope:
yield scope
async def get_service(scope: AsyncScope = Depends(request_scope)) -> RegisterUser:
return await scope.aresolve(RegisterUser)
Full runnable version: examples/fastapi_async.py.
Notes
aresolve()compiles a flatasynccreator that inlines the synchronous parts of the graph and awaits only genuinely async nodes (a graph with no async factories reuses the synchronous compiled creator); unflattenable graphs fall back to an interpreted async walk. The sync fast path is unaffected.- Cycle detection on the async path uses a per-resolution guard, so concurrent resolves don't interfere.
-
A singleton async resource lives until
aclose(). In tests, run the resolve andaclose()in the same event loop —asyncio.run()finalizes suspended async generators when it shuts the loop down.