Context Providers¶
Often, scopes are connected with external events: HTTP requests, messages from a queue, callbacks from a framework. These events can be represented by objects which can be used for dependency creation.
ContextProvider is a provider type that injects runtime context values — framework objects like
requests or websockets, or your own custom context — into dependencies, extracting them from the
container's context registry at resolve time.
In integrations, some context objects (like fastapi.Request, litestar.WebSocket, etc.) are
automatically provided — see Framework Context Objects below.
ContextProvider(context_type, *, scope=Scope.APP, bound_type=UNSET) — context_type may also be
passed as a keyword (context_type=).
Basic Usage¶
Declare a ContextProvider for your context type, supply the value when you build the child container, and any Factory that takes that type as a parameter receives it automatically:
from modern_di import Group, Container, Scope, providers
# Custom context type
class CustomContext:
def __init__(self, user_id: str, tenant_id: str) -> None:
self.user_id = user_id
self.tenant_id = tenant_id
def create_user_info(custom_context: CustomContext) -> dict[str, str]:
return {
"user_id": custom_context.user_id,
"tenant_id": custom_context.tenant_id,
}
class Dependencies(Group):
# Manually defined ContextProvider for custom context
custom_context = providers.ContextProvider(CustomContext, scope=Scope.REQUEST)
# Factory uses the custom context
user_info = providers.Factory(
create_user_info,
scope=Scope.REQUEST,
)
# Provide custom context when building the child container
container = Container(groups=[Dependencies], validate=True)
container.open() # open the root before building children (validates once)
custom_context = CustomContext(user_id="123", tenant_id="abc")
request_container = container.build_child_container(
scope=Scope.REQUEST,
context={CustomContext: custom_context}
)
request_container.open()
# Now resolve the factory — it will receive the custom context automatically
user_info = request_container.resolve_provider(Dependencies.user_info)
# {"user_id": "123", "tenant_id": "abc"}
The provider is bound to a scope (here Scope.REQUEST) and the value is supplied via
build_child_container(context={...}).
When no value is set¶
A ContextProvider reads its value from the context of the container at its bound scope. If nothing was supplied, behavior depends on the call path:
- Resolving it directly (
container.resolve(CustomContext)) raisesContextValueNotSetError(see Migration: direct resolve of an unsetContextProviderraises). - Injecting it into a
Factoryparameter that is notOptional/defaulted raisesArgumentResolutionError.
Annotate the consuming parameter as X | None (or give it a default) if the value can legitimately be absent. See ContextProvider has no value.
Context propagation¶
Context never propagates between containers. A ContextProvider reads the context registry of the container at the provider's own scope — build order is irrelevant.
Scope determines which container is read, not timing
Setting context on a parent container never reaches a child-scoped provider, regardless of when you call set_context:
# ❌ Broken: a REQUEST-scoped provider reads the REQUEST container's registry.
# Setting it on the APP parent has no effect.
app_container = Container(validate=True)
app_container.open()
app_container.set_context(CustomContext, value) # ignored for REQUEST-scoped providers
request_container = app_container.build_child_container(scope=Scope.REQUEST)
For a REQUEST-scoped ContextProvider, set the value on the request container:
# Option A: pass context directly when building the child
request_container = app_container.build_child_container(
scope=Scope.REQUEST, context={CustomContext: value}
)
# Option B: set on the request container after building it
request_container = app_container.build_child_container(scope=Scope.REQUEST)
request_container.set_context(CustomContext, value)
Setting context on the parent only works when the ContextProvider's scope matches the parent's scope.
Framework Context Objects¶
Every framework integration auto-registers ContextProviders for its own request/websocket-like
objects — you never declare a ContextProvider for these yourself. Each integration builds a
per-request (or per-message, or per-connection) child container and sets the framework object as
context on it before your code resolves anything from it. There are two ways to consume that value:
Implicit usage (type-based resolution). Annotate a factory parameter with the framework's
type; because the integration already registered a matching ContextProvider, modern-di resolves
it automatically — the same mechanism as Basic Usage above, just with the
ContextProvider declared by the integration instead of by you. With
FastAPI, the fastapi.Request is injected into each per-request
child container automatically:
from modern_di import Group, Container, Scope, providers
import fastapi
import modern_di_fastapi
def create_request_info(request: fastapi.Request | None = None) -> dict[str, str]:
if request is None: # only at validate() time; the integration always sets it at runtime
return {}
return {"method": request.method, "url": str(request.url)}
class Dependencies(Group):
# Factory uses the request from context (automatically provided by the integration)
request_info = providers.Factory(
create_request_info,
scope=Scope.REQUEST,
)
ALL_GROUPS = [Dependencies]
app = fastapi.FastAPI()
# validate=True stays on: the optional `request` parameter (| None = None) lets
# validation skip it, since fastapi.Request's ContextProvider is only registered
# by setup_di() below — after the container (and its validate()) is built.
container = Container(groups=ALL_GROUPS, validate=True)
modern_di_fastapi.setup_di(app, container)
# The integration creates a REQUEST-scoped child container per request and
# injects the fastapi.Request into its context, so `request` is the real object
# at runtime — never None.
The | None = None on request is what keeps validate=True usable. Validation is
deferred to when the container is opened (the integration opens it as part of the
app lifecycle); annotating request as optional means the check skips it regardless of
whether fastapi.Request's ContextProvider is registered by then — a required
parameter with no provider would raise
ArgumentResolutionError.
A defaulted parameter is skipped by that check, and at runtime the integration has
registered the provider and set the per-request context, so the real Request is
injected — the parameter is None only when resolved with no context set, which the
integration never does per request. The default is load-bearing only at validation
time; it does not change runtime behaviour. (A defaulted Factory parameter keeps
its own disposition in modern-di 3.0 — the 3.0 ContextValueNotSetError affects only
a direct resolve of an unset context type, not a defaulted parameter, which still
falls back to its default.) Prefer this to validate=False, which silences all
validation; reach for validate=False (and validate after setup_di() instead)
only if you'd rather keep the parameter required so a missing context fails loudly.
Explicit usage (provider-based resolution). Every integration also exports the underlying
ContextProvider object itself (e.g. fastapi_request_provider, litestar_request_provider,
aiohttp_request_provider, faststream_message_provider) so you can wire it through kwargs
instead of relying on type-based resolution — useful with skip_creator_parsing=True, or when the
parameter name doesn't match the type:
Each integration's own page has its exact provider names, scopes, and API table: FastAPI, Litestar, Starlette, FastStream, aiohttp.
See also¶
- Factories — how factories receive injected context values.
- Scopes — choosing the scope a
ContextProvideris bound to. - Container —
build_child_containerandset_context. - FastAPI integration — framework-provided context objects.