Skip to content


Provider binding serves two important functions:

  • A way to tell the container how to assemble things that can't be auto-wired, for example interfaces.
  • A way to override dependencies in tests.

Every bind in di consists of:

  • A target callable: this can be a function, an interface / protocol or a concrete class
  • A substitute dependency: an object implementing the DependentBase, usually just an instance of Dependent

This means that binds are themselves dependencies:

import sys
from dataclasses import dataclass

if sys.version_info < (3, 8):
    from typing_extensions import Protocol
    from typing import Protocol

from di import Container, bind_by_type
from di.dependent import Dependent
from di.executors import AsyncExecutor

class DBProtocol(Protocol):
    async def execute(self, sql: str) -> None:

async def controller(db: DBProtocol) -> None:
    await db.execute("SELECT *")

class DBConfig:
    host: str = "localhost"

class Postgres(DBProtocol):
    def __init__(self, config: DBConfig) -> None: =

    async def execute(self, sql: str) -> None:

async def framework() -> None:
    container = Container()
    container.bind(bind_by_type(Dependent(Postgres, scope="request"), DBProtocol))
    solved = container.solve(Dependent(controller, scope="request"), scopes=["request"])
    # this next line would fail without the bind
    async with container.enter_scope("request") as state:
        await solved.execute_async(executor=AsyncExecutor(), state=state)
    # and we can double check that the bind worked
    # by requesting the instance directly
    async with container.enter_scope("request") as state:
        db = await container.solve(
            Dependent(DBProtocol), scopes=["request"]
    assert isinstance(db, Postgres)

In this example we register the Postgres class to DBProtocol, and we can see that di auto-wires Postgres as well!

Binds can be used as a direct function call, in which case they are permanent, or as a context manager, in which case they are reversed when the context manager exits.

Bind hooks

Binding is implemented as hooks / callbacks: when we solve a dependency graph, every hook is called with every dependent and if the hook "matches" the dependent it returns the substitute dependency (otherwise it just returns None).

This means you can implement any sort of matching you want, including:

  • Matching by type (see di.container.bind_by_type)
  • Matching by any subclass (di.container.bind_by_type using the covariant=True parameter)
  • Custom logic, in the form of a bind hook (Container.bind)

For example, to match by parameter name:

import inspect
import typing
from dataclasses import dataclass

from di import Container
from di.api.dependencies import DependentBase
from di.dependent import Dependent
from di.executors import SyncExecutor

class Foo:
    bar: str = "bar"

def match_by_parameter_name(
    param: typing.Optional[inspect.Parameter], dependent: DependentBase[typing.Any]
) -> typing.Optional[DependentBase[typing.Any]]:
    if param is not None and == "bar":
        return Dependent(lambda: "baz", scope=None)
    return None

container = Container()


solved = container.solve(Dependent(Foo, scope=None), scopes=[None])

def main():
    with container.enter_scope(None) as state:
        foo = solved.execute_sync(executor=SyncExecutor(), state=state)
    assert == "baz"