Skip to content

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])
custom_context = CustomContext(user_id="123", tenant_id="abc")
request_container = container.build_child_container(
    scope=Scope.REQUEST,
    context={CustomContext: custom_context}
)

# 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:

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()
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) -> dict[str, str]:
    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()
container = Container(groups=ALL_GROUPS)
modern_di_fastapi.setup_di(app, container)
# setup_di() registers fastapi.Request's ContextProvider, so the graph is complete
# from here on — call validate() after this line, not before.
container.validate()
# 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.

Nothing validates automatically, so the ordering above is what matters: fastapi.Request's ContextProvider only exists once setup_di() has registered it, so calling container.validate() before that line would raise ValidationFailedError — its .errors would carry an ArgumentResolutionError for the required request parameter, since the provider isn't there yet. Call validate() after setup_di(), as above, and a required parameter validates cleanly. See Writing an integration for the same rule from the integration author's side.

If you need to validate the rest of the graph before setup_di() runs — e.g. as part of a narrower, construction-time check — make the parameter optional instead (request: fastapi.Request | None = None), so validate() skips it regardless of whether the connection provider is registered yet; at runtime the integration still injects the real Request, since it always sets the per-request context before resolving. A defaulted Factory parameter keeps its own disposition here too: ContextValueNotSetError (see When no value is set above) affects only a direct resolve of an unset context type, not a defaulted parameter, which still falls back to its default when no context is set.

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:

kwargs={"request": fastapi_request_provider}  # explicit wiring, see Factories: kwargs

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 ContextProvider is bound to.
  • Containerbuild_child_container and set_context.
  • FastAPI integration — framework-provided context objects.