Simple Dependency Injection in Python
May 8, 2021
Dependency injection is an software design pattern that basically amounts to parameterizing every
object by the services that it uses, and forcing the instantiator of an object to supply objects
representing the services needed by the created object.
I'm somewhat torn on dependency injection. On the positive side, decoupling an object from the services
it uses is almost a requirement for any form of sane unit testing, since then you can test an object
by supplying proxy services that can be controlled by the testing environment. On the negative side, it
adds an extra layer of indirection that makes code difficult to read, difficult to maintain, and often
adds visual noise that distracts from what an object does. (Dependency injection often comes with an
attendant architectural infrastructure, of the kind best examplified by the Java Spring Framework.)
Is it possible to get some of the advantages of dependency injection for unit testing without straying too much from how you would normally code?
Here's a positive answer to the question in the context of Python. It is far from an ideal solution,
but it does provide a lightweight form of dependency injection that should suffice for unit testing.
Consider the following simple Example
class that depends on a simple Service
class:
class Example:
def __init__(self, arg):
self._arg = arg
def run(self):
obj = Service(self._arg)
obj.print()
class Service:
def __init__(self, arg):
self._arg = arg + 1
def print(self):
print(f'In default service = {self._arg}')
We want the ability to instantiate Example
with an alternate service Service2
in place of Service
in
some situations (such as testing):
class Service2:
def __init__(self, arg):
self._arg = arg + 100
def print(self):
print(f'In service 2 = {self._arg}')
To do that, we introduce a class decorator injectable
that takes as arguments all the services used
in the class that we want to be "injectable" at instantiation time. That decorator intuitively adds
an instance variable for every service listed and initialized with the given service. The
initialization can be overridden using a keyword argument on the class constructor with the same
name as the service being injected.. To take advantage of this injection, we simply need to adjust
every reference to an "injectable" service within the class so that it uses the instance variable
named after the service. For example:
@injectable(Service)
class Example:
def __init__(self, arg):
self._arg = arg
def run(self):
obj = self.Service(self._arg)
obj.print()
Instantiating the class with the default service does not require any extra work:
>>> e1 = Example(10)
>>> e1.run()
In default service = 11
And instantiating the class with an alternative service simply requires passing the alternative
service during instantiation as a keyword argument for the keyword corresponding to the service to
be injected:
>>> e2 = Example(10, Service=Service2)
>>> e2.run()
In service 2 = 110
The main downside of this approach is the need to qualify every "injectable" service reference with
the self
argument within the class. adding visual noise and a potential failure point if one such
reference is not qualified while others are.
For completeness, here's a reference implementation of the injectable
decorator:
def injectable(*services):
def inject(cls):
original_init = cls.__init__
names = [ service.__name__ for service in services ]
def new_init (self, *args, **kwargs):
kwargs_ = { k: kwargs[k] for k in kwargs if k not in names }
self._services = { service.__name__: service for service in services}
for n in names:
if n in kwargs:
self._services[n] = kwargs[n]
original_init(self, *args, **kwargs_)
def get_attribute(self, attr):
if attr in self._services:
return self._services[attr]
else:
raise Exception('no such attribute')
cls.__init__ = new_init
cls.__getattr__ = get_attribute
return cls
return inject
You can download the full code.
Dependency injection is an software design pattern that basically amounts to parameterizing every object by the services that it uses, and forcing the instantiator of an object to supply objects representing the services needed by the created object.
I'm somewhat torn on dependency injection. On the positive side, decoupling an object from the services it uses is almost a requirement for any form of sane unit testing, since then you can test an object by supplying proxy services that can be controlled by the testing environment. On the negative side, it adds an extra layer of indirection that makes code difficult to read, difficult to maintain, and often adds visual noise that distracts from what an object does. (Dependency injection often comes with an attendant architectural infrastructure, of the kind best examplified by the Java Spring Framework.)
Is it possible to get some of the advantages of dependency injection for unit testing without straying too much from how you would normally code?
Here's a positive answer to the question in the context of Python. It is far from an ideal solution, but it does provide a lightweight form of dependency injection that should suffice for unit testing.
Consider the following simple Example
class that depends on a simple Service
class:
class Example:
def __init__(self, arg):
self._arg = arg
def run(self):
obj = Service(self._arg)
obj.print()
class Service:
def __init__(self, arg):
self._arg = arg + 1
def print(self):
print(f'In default service = {self._arg}')
We want the ability to instantiate Example
with an alternate service Service2
in place of Service
in
some situations (such as testing):
class Service2:
def __init__(self, arg):
self._arg = arg + 100
def print(self):
print(f'In service 2 = {self._arg}')
To do that, we introduce a class decorator injectable
that takes as arguments all the services used
in the class that we want to be "injectable" at instantiation time. That decorator intuitively adds
an instance variable for every service listed and initialized with the given service. The
initialization can be overridden using a keyword argument on the class constructor with the same
name as the service being injected.. To take advantage of this injection, we simply need to adjust
every reference to an "injectable" service within the class so that it uses the instance variable
named after the service. For example:
@injectable(Service)
class Example:
def __init__(self, arg):
self._arg = arg
def run(self):
obj = self.Service(self._arg)
obj.print()
Instantiating the class with the default service does not require any extra work:
>>> e1 = Example(10)
>>> e1.run()
In default service = 11
And instantiating the class with an alternative service simply requires passing the alternative service during instantiation as a keyword argument for the keyword corresponding to the service to be injected:
>>> e2 = Example(10, Service=Service2)
>>> e2.run()
In service 2 = 110
The main downside of this approach is the need to qualify every "injectable" service reference with
the self
argument within the class. adding visual noise and a potential failure point if one such
reference is not qualified while others are.
For completeness, here's a reference implementation of the injectable
decorator:
def injectable(*services):
def inject(cls):
original_init = cls.__init__
names = [ service.__name__ for service in services ]
def new_init (self, *args, **kwargs):
kwargs_ = { k: kwargs[k] for k in kwargs if k not in names }
self._services = { service.__name__: service for service in services}
for n in names:
if n in kwargs:
self._services[n] = kwargs[n]
original_init(self, *args, **kwargs_)
def get_attribute(self, attr):
if attr in self._services:
return self._services[attr]
else:
raise Exception('no such attribute')
cls.__init__ = new_init
cls.__getattr__ = get_attribute
return cls
return inject
You can download the full code.