To tell you the truth, I’m a big fan of dependency injection. One you get to a certain app size (and/or component lifetime requirements), having your dependency instances handled for you is a godsend.

I just don’t like how it works in FastAPI

You see, in FastAPI if you want to inject a component in, say, an endpoint you would do something like def my_endpoint(a=Depends(my_a_factory)), and have your my_a_factory create an instance of a or whatever. Simple, right? And, if a depends on, say, b, you then create a my_b_factory, responsible for creating b, then change the signature of my_a_factory to something like def my_a_factory(b=Depends(my_b_factory)). Easy.

But wait! What if b requires some dependencies itself? Well, I hope you’re using your comfortable keyboard, because you’re gonna have to write and wire up a lot of factories. One for each component. Each one Depends-ing on others. With you managing all their little lifetimes by hand. It’s factories all the way down, friend. All the way down.

And sure, I mean, this approach is fine. You can use it to check user permissions, inject your db session, and stuff. It’s easy to get your head around it.

But for building something more complex? Where class A needs an instance of class B, and B in turn needs C & D instances, and (guess what) D depends on E & F? Nah, man, ain’t nobody got time for that.

And I haven’t even mentioned the plethora of instance lifetimes – say, B, D, & E are singletons, C is per-FastAPI-request, and F is transient, i.e. it’s instantiated every time. Implement this with Depends and you’ll be working on your very own, extremely private, utterly personal, HELL.

So anyway, this is how I ended up looking at DI libraries for Python

There’s not that many Python dependency injection libraries, mind you. Looks like a lot of Python devs are happily building singletons left and right and don’t need to inject no dependencies, while most of the others think DI is all about simplifying unit tests and just don’t see the point of inverting control.

To me though, dependency inversion/injection is all about component lifetime management. I don’t want to care how to instantiate nor how to dispose a dependency. I just want to declare it and then jump straight to using it. And the harder it is for me to use it, i.e. by instantiating it and its “rich” dependency tree, disposing each one when appropriate, etc, the more likely that I won’t even bother at all. Simple things should be simple.

So as I said, there’s not a lot of DI frameworks in Python. Just take a look at this Awesome Dependency Injection in Python, it’s depressing, really (the content, not the list, the list is cool). Only 3 libraries have more than 1k stars on Github. Some of the smaller ones are cute, others not so much.

Out of the three, the most popular seemed to be python-dependency-injector, but I didn’t like the big development gap between Dec 2022 and Aug 2024. Development seems to have picked up recently, but I’ve decided to give it a little more time to settle. It has a bunch of providers, but it wasn’t clear to me how I would get a per-request lifetime. Their FastAPI example looks a bit weird to me, I’m not a fan of those Depends(Provide[Container.config.default.query]) calls (why should ALL my code know where I’m configuring my dependencies?!?).

The second most popular one is returns, which looks interesting and a bit weird, but ultimately doesn’t seem to be what I’m after.

The third one is injector. Not terribly updated, but not abandoned either. I like that I can define the lifetimes of my components in a single place. I..kinda dislike that I need to decorate all my injectable classes with @inject but beggars can’t be choosers, am I right? The documentation is not nearly as good as python-dependency-injector’s. I can couple it with fastapi-injector to get request-scoped dependencies.

In the end, after looking at a gazillion other options, I went with the injector + fastapi-injector combo – it covered most of my pain points (single point for defining my dependencies and their lifetimes, easy to integrate with FastAPI, reasonably up to date), and the drawbacks (that pesky @inject) were minimal.

Here’s how I set it up to handle my convoluted example above

Where class A needs an instance of class B, and B in turn needs C & D instances, and (guess what) D depends on E & F

First, the classes. The only thing they need to know is that they’ll be @injected somewhere, and, if they require some dependencies, to declare and annotated them.

# classes.py
from injector import inject

@inject
class F
    def __init__(self)
        pass

@inject
class E
    def __init__(self)
        pass

@inject
class D
    def __init__(self, e: E, f: F):
        self.e = e
        self.f = f

@inject
class C:
    def __init__(self)
        pass

@inject
class B:
    def __init__(self, c: C, d: D):
        self.c = c
        self.d = d

@inject
class A:
    def __init__(self, b: B):
        self.b = b

say, B, D, & E are singletons, C is per-FastAPI-request, and F is transient, i.e. it’s instantiated every time.

The lifetimes are defined in one place and one place only, while the rest of the code doesn’t know anything about this.

# dependencies.py

from classes import A, B, C, D, E, F
from fastapi_injector import request_scope
from injector import Module, singleton, noscope

class Dependencies(Module):
    def configure(self, binder):
        binder.bind(A, scope=noscope)
        binder.bind(B, scope=singleton)
        binder.bind(C, scope=request_scope)
        binder.bind(D, scope=singleton)
        binder.bind(E, scope=singleton)
        binder.bind(F, scope=noscope)

        # this one's just for fun 🙃
        binder.bind(logging.Logger, to=lambda: logging.getLogger())

Then, attach the injector middleware to your app, and start injecting dependencies in your routes with Injected.

# main.py

from fastapi_injector import InjectorMiddleware, attach_injector
from injector import Injector

app = FastAPI()

injector = Injector(Dependencies())
app.add_middleware(InjectorMiddleware, injector=injector)
attach_injector(app, injector)

@app.get("/")
def root(a: A = Injected(A)):
    pass

Not too shabby. It’s not a perfect solution, but it’s quite close to what I had gotten used to in .NET land. I’m sticking with it for now.

Notable mentions

  • Dishka doesn’t look half bad, it may be worth investigating in the future
  • Kink looks interesting as well, but I haven’t seen a way to manage the components’ lifetimes