Comparison of Dependency Injection Libraries in Python, and my favorite one
Comparison of Dependency Injection Libraries in Python, and my favorite one #
About Dependency Injection #
In one of my last posts, I gave an introduction to DI pattern: why it is crucial for software projects & how it differs from Service Locator patterns. Long story short using DI supports Low Coupling & High Cohesion in turn giving us great testability and configurability.
Short overview of available DI libraries in Python #
DI is not a commonly used concept in the Python community, actually often it’s quite neglected, because “Python is not Java”, obviously, but this does not mean that we should not strive to write good, testable code, and that’s the reason why we should use DI. The popularity of Django, which suggests in its documentation a design that does not support any Inversion of Control also does not help with promoting good practices. Lately, FastAPI gain popularity, and it’s the first (to my knowledge) web framework which supports native Dependency Injection Because of all of these reasons, there are just a few DI containers available, some of them do not even hold the definition of a good container.
What describes a good DI container #
A good DI container:
- must not be a Service Locator, and that’s easier to get as one might think, just go back to my previous post about this topic
- must not be configurable globally (as a globally available instance), because it introduces problems with the reconfiguration
- must support shared dependencies, so we wouldn’t need to exploit the Singleton Pattern
- must support use of profiles, so we can configure it accordingly on different environments
- should support the Decorator Pattern
Overview of available solutions #
-
https://github.com/ivankorobkov/python-inject disqualified because it uses globally available configuration, and descriptors to pull instances from it, which makes it more of a Service Locator. It also uses Singletons as the default scope which is even worse.
-
https://github.com/google/pinject from Google. Feels highly outdated and so it is, as the documentation is still written for Python2. Also, it uses Singletons as default, so even though the container is an instance created “in place” it still returns the same instances again and again. It does not support any reconfiguration solutions for tests.
-
https://github.com/alecthomas/injector well… I can’t tell anything very bad about it, but nothing good either. My problem with it is its interface, which for me isn’t very pleasant to use.
-
http://python-dependency-injector.ets-labs.org/introduction/di_in_python.html Very good library, actively supported, it’s the only drawback from my point of view is the boilerplate which is necessary to create even a small app with it. But besides that it has many pros: support for multiple types of scopes (default is shared instance, but it includes also very interesting
Configuration
orResource
); it allows defining small containers, no global configuration, supports modularization; it allows to override the configuration for e.g. tests purpose; provideswire
mechanism to integrate with popular frameworks, even Django. A really good choice!
The winner - dependencies aka Injector #
My personal winner is https://github.com/proofit404/dependencies which I’m used to cal Injector, because that’s the name of the main class.
Fundamentals #
- dependencies are matched by
__init__
attribute names (kwargs) - container returns always a fresh instance when called for one
- it uses shared instance scope by default, and it’s the only available scope out of the box.
These simple mechanisms allowed the author of the library to create a really nice abstraction over the configuration of the container and its internals. Actually, everything is very nicely encapsulated, and the user must remember about only these 3 points.
One might ask why kwargs and not types as it usually is in other libraries? The author justifies it by superior readability of his approach which would not be possible with the usage of types:
from typing import List
class Container:
List = list
Container.List
is just a string under the hood. Well, it’s really hard to argue with the author, because this design really is pretty, just check it below!
In practice #
This is how the code can look like in practice
class CustomerFeedContainer(Injector):
bml_client = BMLAPIClient
drunken_water_info_provider = BMLDrunkenWaterInfoProvider
# entry points
query_entry = CustomerFeedQueryEntry
# queries
GetDrunkenWaterDataForCustomerQuery = GetDrunkenWaterDataForCustomerHandler
@dataclass
class CustomerFeedQueryEntry(InjectorQueryEntry):
GetDrunkenWaterDataForCustomerQuery: GetDrunkenWaterDataForCustomerHandler
@dataclass
class BMLDrunkenWaterInfoProvider:
bml_client: BMLAPIClient
[...]
@dataclass
class GetDrunkenWaterDataForCustomerHandler(
QueryHandler[GetDrunkenWaterDataForCustomerQuery, DrunkenWaterView]
):
drunken_water_info_provider: DrunkenWaterInfoProvider
[...]
CustomerFeedContainer
stores the configuration, a call to it’s getattr
method will return a built instance of one of the defined dependencies. Let’s say we want to get an instance of BMLDrunkenWaterInfoProvider
which uses BMLAPIClient
underneath. We just do:
provider_instance = CustomerFeedContainer.drunken_water_info_provider
what happens under the hood is the match of CustomerFeedContainer.bml_client
to BMLDrunkenWaterInfoProvider.__init__(self, bml_client)
, which allows us to provide different implementations of BMLAPIClient
as long as it matches the kwarg name and the interface of course.
Pros #
- very easy to use, thanks to it’s design the DI configuration is very simple to do
- it allows to override the configuration for tests with the use of
Injector()
or previouslyInjector.let()
- it does introduce to our code any hard dependencies like decorators or descriptors as other libraries usually do. We only need to watch out for override usage in tests, but that can be abstracted out too.
- concrete composition root in the form of
Injector
class - supports modularization - nice documentation
Cons #
- its simplicity is also a con, as it does not provide too many features. If you need a Singleton you have to think about how to achieve that, but that’s a pro too if you’d ask me - thinking before doing is good
- does not provide any integration for frameworks
- returned instances aren’t typed
- only one maintainer, but very active and supportive
Patterns #
Composition root #
When we do not have a globally stored configuration/container as in one of the fore-mentioned libraries we should strive for small purposeful containers which will be our composition roots for e.g. modules of our application. A good idea is also to separate the business logic dependencies from the infrastructural ones and couple them together somewhere at the root of our application.
class DI(Injector):
db_session = DBSession
class Config(Injector):
db_uri = "postgres://localhost"
DI.db_session # raises DepencencyError
(DI & Config).db_session # OK!
class DIAndConfig(DI, Config): ...
DIAndConfig.db_session # OK!
too many dependencies anti-pattern #
It’s not hard to predict that because the container builds always a fresh instance, then we should care about having a small number of dependencies in the classes. What it comes down to in practice is having specific ApplicationService
s or as I prefer CommandHandler
s which do one thing.
Container encapsulation #
As I wrote in one of my last posts injecting the container is a very bad thing to do, because it becomes more of a service locator than a DI solution. However, depending on how our app’s architecture is layered, or how we do the assembly of our whole app, for example, based on the framework we’re using for web, or when our app is composed of modules we might need to “call” our container from a client class. My solution to that problem is encapsulating the container behind a nice abstraction:
class Module(ABC):
"""
Module encapsulates a module, providing only 2 actions:
handle - for commands/queries,
override - for overriding module's setup, particularly dependencies.
"""
@abstractmethod
def handle(self, action: Union[Query, Command]) -> Any:
pass
@abstractmethod
def override(self: TMod, **dependencies: Any) -> TMod:
pass
class InjectorModule(Module):
def __init__(self, container: Type[Injector]) -> None:
self.container = container
def handle(self, action: Union[Query, Command]) -> Any:
if isinstance(action, Query):
return cast(InjectorQueryEntry, self.container.query_entry).handle(action)
if isinstance(action, Command):
return cast(InjectorCommandEntry, self.container.command_entry).handle(action)
raise ValueError("Action must be a Command or Query")
def override(self: TInjectorMod, **dependencies: Any) -> TInjectorMod:
return self.__class__(self.container.let(**dependencies))
@classmethod
def new(cls: Type[TInjectorMod], container: Type[Injector]) -> TInjectorMod:
return cls(container)
CustomerActivities = InjectorModule.new(CustomerActivitiesContainer)
That way a Controller
element of the app can have direct access only to a Module
with a very specific and small interface, and not the whole Injector
container.
Encapsulating the container is also crucial from the point of view of it’s exchange to a different solution some day. It mitigates one of the cons that I mentioned before: it can hide the calls to override mechanism.
Singletons or long-living instances #
Sometimes we might want to use the same instance (or rather have it returned by the Injector) through many calls to the DI container, an example could be the same connection to a database. There are some ways to deal with that.
We can create the instance out of the scope of the container and then pass it as a kwarg using the override mechanism:
class DI(Injector):
singleton = None
a_singleton = SomeService()
di = DI.let(singleton=a_singleton)
assert di.singleton is di.singleton is a_singleton
Or we can use “the real” Singleton Pattern:
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class DBSession(metaclass=Singleton):
def __init__(self, db_uri):
self.db_uri = db_uri
self._is_connected = False
def connect(self):
self._is_connected = True
@property
def is_connected(self):
return self._is_connected
class DI(Injector):
db_session = DBSession
db_uri = "postgres://localhost"
DI.db_session.connect()
assert DI.db_session.is_connected
In one of my projects I used such a nice piece of code to have the UnitOfWork
live through several calls to the Module
which holds it - to be able to check the emitted DomainEvents
(out of scope here)
class FakeInjectorModule(InjectorModule):
"""
A Module which uses the same UoW instance, providing a scoped Singleton
object. This allows to have:
- persistence in FakeRepositories
- checking if UoW was committed
- checking published Events
"""
_unit_of_work: FakeUnitOfWork
def __init__(self, container: Injector) -> None:
self._unit_of_work = container.unit_of_work
self.container = container.let(unit_of_work=self._unit_of_work)
@property
def unit_of_work(self) -> FakeUnitOfWork:
return self._unit_of_work
Of course, I think that building the instance outside of the container is a much better, reliable, and predictable approach than using the Singleton. It’s common to forget that we are using a Singleton…
Profiles #
To configure dynamically the dependencies, for example, based on environmental variables or something you can use a Factory-like custom provider and @value
decorator. Below I used string profiles
, but it does not matter where the profiles
come from, it can be envs
from dependencies import Injector, this, value
from dataclasses import dataclass
@dataclass
class DB:
name: str
@dataclass
class DBProvider:
profiles: str
def provide(self):
if "dev" in self.profiles:
return DB("DEV DB")
else:
return DB("PROD DB")
class DI(Injector):
db = this.db_provider
@value
def db_provider(profiles: str = ""):
return DBProvider(profiles).provide()
dev_db = DI.db
prod_db = DI.let(profiles="dev").db
Upcoming enhancements #
easy Decorator Pattern support #
from dependencies import Injector, decorate
class CommandLoggerHandler(CommandHandler):
logger: Logger
handler: CommandHandler
def handle(cmd: Command):
self.logger.log(f"handle command {cmd}")
return self.handler(cmd)
class CustomerActivitiesContainer(Injector):
unit_of_work = SqlAlchemyCustomerActivitiesUnitOfWork
logger = StructuredLogger
# entry points
command_entry = CustomerActivitiesCommandEntry
query_entry = CustomerActivitiesQueryEntry
# commands
ScheduleItemCommand = decorate(CommandLoggerHandler, ScheduleItemHandler)
MarkItemAsDoneCommand = MarkItemAsDoneHandler
# queries
GetScheduledItemQuery = decorate(QueryLoggerHandler, GetScheduledItemQueryHandler)
GetAgendaItemsQuery = decorate(QueryLoggerHandler, GetAgendaItemsQueryHandler)
I the future we might get such a nice way to apply the Decorator Pattern. It’s supported currently by using this
proxy and inner-class Injector containers which makes it very hard to read.
Summary #
DI in Python is not a popular subject. We have 3 libraries which match the definition:
- alecthomas/injector I do not like its interface, but I know it’s being used in production by some people
- ets-labs/python-dependency-injector very expanded library, with constant support, the problem is it’s boilerplate, if that does not bother you, then it’s a great choice
- proofit404/dependencies (Injector) simple, but provided all the necessary features. If you need something that’s not provided then just think about the design in your application, cause the flow might be somewhere there. Beautiful configuration. Perfect match for agile projects ;)