Skip to content

di: pythonic dependency injection

di is a modern dependency injection system, modeled around the simplicity of FastAPI's dependency injection.

Key features:

  • Intuitive: simple API, inspired by FastAPI.
  • Succinct: declare what you want, and di figures out how to assmble it using type annotations.
  • Correct: tested with MyPy: value: int = Depends(returns_str) gives an error.
  • Scopes: inspired by pytest scopes, but defined by users (no fixed "request" or "session" scopes).
  • Flexible: decoupled internal APIs give you the flexibility to customize wiring and execution.
  • Performant: di can execute dependencies in parallel, move sync dependencies to threads and cache results. Performance critical parts are written in 🦀.

Installation

$ pip install di
---> 100%

Warning

This project is a work in progress. Until there is 1.X.Y release, expect breaking changes.

Examples

Simple Example

Here is a simple example of how di works:

from dataclasses import dataclass

from di import Container, Dependant


class A:
    ...


class B:
    ...


@dataclass
class C:
    a: A
    b: B


def main():
    container = Container()
    c = container.execute_sync(container.solve(Dependant(C)))
    assert isinstance(c, C)
    assert isinstance(c.a, A)
    assert isinstance(c.b, B)

Why do I need dependency injection in Python? Isn't that a Java thing?

Dependency injection is a software architecture technique that helps us achieve inversion of control and dependency inversion (one of the five SOLID design principles).

It is a common misconception that traditional software design principles do not apply to Python. As a matter of fact, you are probably using a lot of these techniques already!

For example, the transport argument to httpx's Client (docs) is an excellent example of dependency injection. Pytest, arguably the most popular Python test framework, uses dependency injection in the form of pytest fixtures.

Most web frameworks employ inversion of control: when you define a view / controller, the web framework calls you! The same thing applies to CLIs (like click) or TUIs (like Textual). This is especially true for many newer webframeworks that not only use inversion of control but also dependency injection. Two great examples of this are FastAPI and BlackSheep.

For a more comprehensive overview of Python projectes related to dependency injection, see Awesome Dependency Injection in Python.

Project Aims

This project aims to be a general dependency injection system, with a focus on providing the underlying dependency injection functionality for other libaries.

In other words, while you could use this as your a standalone dependency injection framework, you may find it to be a bit terse and verbose. There are also much more mature standalone dependency injection frameworks; I would recommend at least looking into python-dependency-injector since it is currently the most popular / widely used of the bunch.

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 it's users via di.

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

from di import Container, Dependant


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


async def web_framework():
    container = Container()
    solved = container.solve(Dependant(controller))
    res = await container.execute_async(solved, values={Request: Request(1)})
    assert res == 2


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

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


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

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 framwork.

from di import Container, Dependant


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


async def web_framework():
    container = Container()
    solved = container.solve(Dependant(controller))
    res = await container.execute_async(solved, values={Request: Request(1)})
    assert res == 2


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

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


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

Next, we "solve" the users endpoint:

from di import Container, Dependant


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


async def web_framework():
    container = Container()
    solved = container.solve(Dependant(controller))
    res = await container.execute_async(solved, values={Request: Request(1)})
    assert res == 2


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

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


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

This should happen once, maybe at app startup. The framework can then store the solved object, which contains all of 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 do the least amount of work possible for each incoming request.

Finally, we execute the endpoint for each incoming request:

from di import Container, Dependant


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


async def web_framework():
    container = Container()
    solved = container.solve(Dependant(controller))
    res = await container.execute_async(solved, values={Request: Request(1)})
    assert res == 2


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

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


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

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 "bind" providers, which is covered in the binds section of the docs.