Skip to content

Examples

Simple Example

Here is a simple example of how di works:

from dataclasses import dataclass

from di import Container
from di.dependent import Dependent
from di.executors import SyncExecutor


class A:
    ...


class B:
    ...


@dataclass
class C:
    a: A
    b: B


def main():
    container = Container()
    solved = container.solve(Dependent(C, scope="request"), scopes=["request"])
    with container.enter_scope("request") as state:
        c = solved.execute_sync(executor=SyncExecutor(), state=state)
    assert isinstance(c, C)
    assert isinstance(c.a, A)
    assert isinstance(c.b, B)

You will notice that di "auto-wired" C: we didn't have to tell it that C depends on A and B, or how to construct A and B, it was all inferred from type annotations.

In the wiring and provider registration chapters, you'll see how you can customize this behavior to tell di how to inject things like abstract interfaces or function return values.

In-depth example

With this background in place, let's dive into a more in-depth example.

In this example, we'll look at what it would take for a web framework to provide dependency injection to its users via di.

Let's start by looking at the User's code.

from typing import Any, Callable

from di import Container
from di.dependent import Dependent
from di.executors import SyncExecutor


# Framework code
class Request:
    def __init__(self, value: int) -> None:
        self.value = value


class App:
    def __init__(self, controller: Callable[..., Any]) -> None:
        self.container = Container()
        self.solved = self.container.solve(
            Dependent(controller, scope="request"),
            scopes=["request"],
        )
        self.executor = SyncExecutor()

    def run(self, request: Request) -> int:
        with self.container.enter_scope("request") as state:
            return self.solved.execute_sync(
                values={Request: request},
                executor=self.executor,
                state=state,
            )


# User code
class MyClass:
    def __init__(self, request: Request) -> None:
        self.value = request.value

    def add(self, value: int) -> int:
        return self.value + value


def controller(myobj: MyClass) -> int:
    return myobj.add(1)


def main() -> None:
    app = App(controller)
    resp = app.run(Request(1))
    assert resp == 2
    resp = app.run(Request(2))
    assert resp == 3


if __name__ == "__main__":
    main()

As a user, you have very little boilerplate. In fact, there is not a single line of code here that is not transmitting information.

Now let's look at the web framework side of things. This part can get a bit complex, but it's okay because it's written once, in a library.

First, we'll need to create a Container instance. This would be tied to the App or Router instance of the web framework.

from typing import Any, Callable

from di import Container
from di.dependent import Dependent
from di.executors import SyncExecutor


# Framework code
class Request:
    def __init__(self, value: int) -> None:
        self.value = value


class App:
    def __init__(self, controller: Callable[..., Any]) -> None:
        self.container = Container()
        self.solved = self.container.solve(
            Dependent(controller, scope="request"),
            scopes=["request"],
        )
        self.executor = SyncExecutor()

    def run(self, request: Request) -> int:
        with self.container.enter_scope("request") as state:
            return self.solved.execute_sync(
                values={Request: request},
                executor=self.executor,
                state=state,
            )


# User code
class MyClass:
    def __init__(self, request: Request) -> None:
        self.value = request.value

    def add(self, value: int) -> int:
        return self.value + value


def controller(myobj: MyClass) -> int:
    return myobj.add(1)


def main() -> None:
    app = App(controller)
    resp = app.run(Request(1))
    assert resp == 2
    resp = app.run(Request(2))
    assert resp == 3


if __name__ == "__main__":
    main()

Next we solve all of our endpoints/controllers (in this case just a single one). This should happen once, maybe at application startup, and then you should save the solved object, which contains all the information necessary to execute the dependency (dependency being in this case the user's endpoint/controller function). This is very important for performance: we want to do the least amount of work possible for each incoming request.

from typing import Any, Callable

from di import Container
from di.dependent import Dependent
from di.executors import SyncExecutor


# Framework code
class Request:
    def __init__(self, value: int) -> None:
        self.value = value


class App:
    def __init__(self, controller: Callable[..., Any]) -> None:
        self.container = Container()
        self.solved = self.container.solve(
            Dependent(controller, scope="request"),
            scopes=["request"],
        )
        self.executor = SyncExecutor()

    def run(self, request: Request) -> int:
        with self.container.enter_scope("request") as state:
            return self.solved.execute_sync(
                values={Request: request},
                executor=self.executor,
                state=state,
            )


# User code
class MyClass:
    def __init__(self, request: Request) -> None:
        self.value = request.value

    def add(self, value: int) -> int:
        return self.value + value


def controller(myobj: MyClass) -> int:
    return myobj.add(1)


def main() -> None:
    app = App(controller)
    resp = app.run(Request(1))
    assert resp == 2
    resp = app.run(Request(2))
    assert resp == 3


if __name__ == "__main__":
    main()

Finally, we execute the endpoint for each incoming request:

from typing import Any, Callable

from di import Container
from di.dependent import Dependent
from di.executors import SyncExecutor


# Framework code
class Request:
    def __init__(self, value: int) -> None:
        self.value = value


class App:
    def __init__(self, controller: Callable[..., Any]) -> None:
        self.container = Container()
        self.solved = self.container.solve(
            Dependent(controller, scope="request"),
            scopes=["request"],
        )
        self.executor = SyncExecutor()

    def run(self, request: Request) -> int:
        with self.container.enter_scope("request") as state:
            return self.solved.execute_sync(
                values={Request: request},
                executor=self.executor,
                state=state,
            )


# User code
class MyClass:
    def __init__(self, request: Request) -> None:
        self.value = request.value

    def add(self, value: int) -> int:
        return self.value + value


def controller(myobj: MyClass) -> int:
    return myobj.add(1)


def main() -> None:
    app = App(controller)
    resp = app.run(Request(1))
    assert resp == 2
    resp = app.run(Request(2))
    assert resp == 3


if __name__ == "__main__":
    main()

When we do this, we provide the Request instance as a value. This means that di does not introspect at all into the Request to figure out how to build it, it just hands the value off to anything that requests it. You can also directly register providers, which is covered in the provider registration section of the docs.

You'll also notice the executor parameter. As you'll see in the [architecture] chapter, one of the fundamental design principles in di is to decouple wiring, solving and execution. This makes it trivial to, for example, enable concurrent execution of dependencies using threads, asynchronous task groups or any other execution paradigm you want.