You are on page 1of 18

Member-only story

SOFTWARE ARCHITECTURE PATTERNS WITH PYTHON

Dependency Injection in Python


Building flexible and testable architectures in Python

Patrick Kalkman · Follow


Published in ITNEXT
13 min read · Apr 14

Listen Share More

Building systems using dependency injection. Image by Midjourney, prompt by author

Python’s growing popularity has resulted in the development of larger and more
intricate projects. This expansion has driven developers to explore high-level
software design patterns, such as those in domain-driven design (DDD).

However, implementing these patterns in Python can pose challenges.


This hands-on series is designed to equip Python developers with practical
examples, emphasizing tried-and-tested architectural design patterns to manage
application complexity effectively.

In this series installment, we will delve into the concepts of Dependency Injection
and its implementation in Python, providing valuable insights for developers
looking to enhance their projects.

All the code discussed in this article is within the accompanying GitHub repository.
The repository provides a convenient way to access and explore the examples in
more detail.

Dependency Injection
Dependency Injection (DI) is a design pattern that encourages loose coupling,
maintainability, and testability within software applications.

DI and DI frameworks have long been popular in statically typed languages like Java
and C#. However, their necessity in dynamic languages like Python has been
debated. Python’s inherent features, such as duck-typing, already offer some
benefits associated with DI.

We won’t focus on this debate but demonstrate practical applications of DI and DI


frameworks within the Python ecosystem. This will help you understand their
potential benefits and use cases.

We will use the example application we built during the previous article about the
Repositories and Unit Of Work patterns.
What exactly is dependency injection?
Dependency Injection (DI) is a design pattern in software development that makes
it easier to manage the relationships between different components or objects in a
program.

In simple terms, it’s a way to provide the necessary dependencies (services, objects,
or values) to a component from an external source rather than having the
component create or find them on its own.
Imagine you’re building a toy car, and instead of having each part (like wheels,
body, and engine) find or create its screws, you provide them from the outside. This
makes it easier to change the screws or reuse the parts in different combinations
without changing the parts themselves.
What are the advantages of dependency injection?
Dependency Injection helps with the following:

1. Flexibility: It makes it easier to change or swap dependencies without


modifying the components that use them.

2. Reusability: Components become more reusable because they’re not tightly


coupled to specific dependencies.

3. Testability: It’s easier to test components by providing mock dependencies


during testing.

What types of dependency injection are there?


There are three common ways to implement Dependency Injection. We will outline
each approach and provide an example for each. Before diving into the examples,
let’s look at a Car class implementation without dependency injection.

class Car:
def __init__(self):
self.engine = Engine()

def start(self):
return self.engine.start()

In this implementation, the Car class creates an Engine object in its constructor,
resulting in tight coupling between the classes. Now, let's explore the three
dependency injection techniques along with examples to improve this code.

1. Constructor Injection: In this approach, the dependencies of a class are


supplied through its constructor. In the example below, the Car class depends
on the Engine class. By using constructor injection, the Car class receives an
instance of the Engine as a constructor argument, thus eliminating the need for
the Car class to create the Engine instance itself. This way, the Car class
remains dependent on the Engine class but no longer has the responsibility of
instantiating it.

class Car:
def __init__(self, engine):
self.engine = engine

def start(self):
return self.engine.start()

2. Setter Injection: The setter methods in the class provide the dependencies. In the
example below, the dependency of the Car, the engine is injected via the set_engine

method.

class Car:
def __init__(self):
self.engine = None

def set_engine(self, engine):


self.engine = engine

3. Method Parameter Injection: The dependencies are provided as parameters to


the methods that use them. In the example below, the dependency of the car, the
engine, is injected through the start method.

class Car:
def start(self, engine):
return engine.start()

Each approach has its advantages and use cases. Constructor and setter injections
are the most commonly used, as they balance flexibility and simplicity.
Open in app
Implementing Dependency Injection in Python
In the previous chapter, we discussed various types of dependency injection that
can be implemented in Python.

In this chapter, we will focus on constructor injection, as it ensures that the object’s
state is correctly initialized. By injecting dependencies through the constructor, we
can be confident that all dependencies are correctly set up.

The figure presented below illustrates the components we will use throughout this
article. We have a single Use Case called CreatePersonAndOrderUseCase that creates an
Order and a Person and stores them in an SQLite database using a single
transaction.
In our project, we utilize a single main file responsible for constructing and
injecting all dependencies into the use case. You can refer to the source code below
for a detailed understanding of the implementation.

@contextmanager
def create_database_connection():
db_connection = sqlite3.connect("./db/data.db")
try:
yield db_connection
finally:
db_connection.close()

with create_database_connection() as conn:


connection = SQLiteConnection(conn)
person_repository = SQLitePersonRepository(conn)
order_repository = SQLiteOrderRepository(conn)

unit_of_work = UnitOfWork(connection, person_repository,


order_repository)
create_use_case = CreatePersonAndOrderUseCase(unit_of_work)

new_person = Person(name="John Doe", age=30)


new_order = Order(person_id=None, order_date="2023-04-03",
total_amount=100.0)

person, order = create_use_case.execute(new_person, new_order)

When we create a dependency graph based on the provided code, the resulting
visualization appears as follows:
Dependency graph of the main source code: A visualization generated using Graphviz by the author.

The visualization illustrates that CreatePersonAndOrderUseCase at the bottom depends


on the UnitOfWork component, which depends on three specific dependencies:
SQLiteConnection , SQLitePersonRepository , and SQLiteOrderRepository .

Examining the source code, we can see that when initializing the
CreatePersonAndOrderUseCase , we inject an instance of the UnitOfWork class.
Similarly, while creating the UnitOfWork , we inject instances of SQLiteConnection ,

SQLitePersonRepository , and SQLiteOrderRepository .

Manual dependency injection


This process is referred to as manual dependency injection. It involves explicitly
creating and injecting all instances into the required classes, ensuring a clear
separation of concerns, and enhancing code maintainability.

As the application grows in complexity and size, managing dependencies through


manual dependency injection can become increasingly tedious and challenging to
maintain.

Using the (Fluent) Builder pattern


An option to improve maintainability is the use of the Builder pattern. The Builder
pattern is a creational design pattern that separates the construction of complex
objects from their representation.

This allows the same construction process to create different object representations
through a step-by-step, chainable interface.

See below for an implementation of the UseCaseBuilder , which employs the Fluent
Builder pattern, a specialized version of the Builder pattern, to build an instance of
the CreatePersonAndOrderUseCase class.

class UseCaseBuilder:
def __init__(self):
self._connection = None
self._person_repository = None
self._order_repository = None

def with_connection(self, connection):


self._connection = connection
return self
def with_person_repository(self, person_repository):
self._person_repository = person_repository
return self

def with_order_repository(self, order_repository):


self._order_repository = order_repository
return self

def build(self):
if not self._connection or not self._person_repository or not self._ord
raise ValueError("All dependencies must be provided before building

unit_of_work = UnitOfWork(self._connection, self._person_repository, se


return CreatePersonAndOrderUseCase(unit_of_work)

# Usage
with create_database_connection() as conn:
builder = UseCaseBuilder()

use_case = (
builder.with_connection(SQLiteConnection(conn))
.with_person_repository(SQLitePersonRepository(conn))
.with_order_repository(SQLiteOrderRepository(conn))
.build()
)

new_person = Person(name="John Doe", age=30)


new_order = Order(person_id=None, order_date="2023-04-03", total_amount=100

person, order = use_case.execute(new_person, new_order)

Another option is to use a dependency injection framework. A dependency


injection (DI) framework is a library or tool that helps manage and automate the
dependency injection process in Python applications.

In the upcoming chapter, we will explore two distinct Python Dependency Injection
(DI) frameworks and demonstrate their usage. Additionally, we will guide you
through creating a simple, custom DI framework tailored to your needs.

Using DI Frameworks in Python


In the previous chapter, we demonstrated how dependency injection works and
how managing dependencies in larger applications can become increasingly
tedious and challenging.
A dependency injection (DI) framework simplifies dependency management and
enhances code maintainability. The framework creates and supplies dependencies
to the required components, reducing manual dependency management and
repetitive code.

Given Python's numerous dependency injection frameworks, we focus on two more


popular ones based on their GitHub star count.

We will explore three different approaches to managing dependencies. First, we


will create a custom DI solution to understand the inner workings of DI frameworks
better. After that, we will examine the Python DI frameworks: Dependency Injector
and Injector, and compare their features and implementations.

A simple custom DI container


Most dependency injection (DI) frameworks utilize the concept of a container. You
register all of your application’s dependencies within this container. When you need
an instance of a class, instead of creating it directly, you request it from the
container.

If the requested instance has dependencies, the container will automatically create
and inject them as needed.

Below is our very first simple custom DI container implementation. It features two
key methods: register and resolve . The register method allows you to add types
to the container, while the resolve method creates and returns an instance of a
specified type and its dependencies.

import inspect

class SimpleContainer:
def __init__(self):
self._registry = {}

def register(self, cls):


self._registry[cls] = cls

def resolve(self, cls):


if cls not in self._registry:
raise ValueError(f"{cls} is not registered in the container.")

target_cls = self._registry[cls]
constructor_params = inspect.signature(target_cls.__init__).parameters.
dependencies = [
self.resolve(param.annotation)
for param in constructor_params
if param.annotation is not inspect.Parameter.empty
]
return target_cls(*dependencies)

The resolve method in the SimpleContainer begins by checking if the requested


class type is available in the registry dictionary. If the class type is found, it employs
the inspect.signature method to obtain all the constructor parameters for the
specified type. Next, the method uses a list comprehension to recursively call
resolve for each constructor argument, ensuring that all dependencies are
resolved.

Finally, the resolve method creates and returns an instance of the requested class
type, with all its dependencies properly injected.

A caveat: handling base classes as constructor arguments


Our SimpleContainer works seamlessly when base classes are not used as
constructor arguments. However, in our previous examples, base classes such as
BaseRepository were utilized in constructor arguments. In such cases, the current
implementation of the SimpleContainer does not work correctly. It does not account
for resolving derived classes when a base class is expected.

An enhanced custom DI container:


To properly manage base classes and their derived classes, we must extend the DI
container with additional logic to handle them as constructor arguments.

The improved Container implementation, as demonstrated below, incorporates the


necessary functionality to accommodate base class constructor arguments, making
it more versatile and suitable for a broader range of use cases.

Compared to the previously shown SimpleContainer, the enhanced Container class


now features an updated register method that accepts two arguments: the type and
the implementation to use.

import inspect
class Container:
def __init__(self):
self._registry = {}

def register(self, dependency_type, implementation=None):


if not implementation:
implementation = dependency_type

for base in inspect.getmro(implementation):


if base not in (object, dependency_type):
self._registry[base] = implementation

self._registry[dependency_type] = implementation

def resolve(self, dependency_type):


if dependency_type not in self._registry:
raise ValueError(f"Dependency {dependency_type} not registered")
implementation = self._registry[dependency_type]
constructor_signature = inspect.signature(implementation.__init__)
constructor_params = constructor_signature.parameters.values()

dependencies = [
self.resolve(param.annotation)
for param in constructor_params
if param.annotation is not inspect.Parameter.empty
]

return implementation(*dependencies)

The new part of the register implementation is the for loop using inspect.getmro . It
iterates through the provided implementation class's method resolution order
(MRO). The MRO is the order in which Python looks for a method in the class
hierarchy. This is done using the inspect.getmro() function that returns a tuple of
classes from which the given class is derived.

Then the method checks if the current base class from the MRO is not object and
not the original dependency_type . If both conditions are met, the method registers
the implementation class for the current base class in the _registry . This allows the
container to resolve the dependency for the specific dependency_type and any
intermediate base classes in the class hierarchy.

With this enhanced Container, we can apply it to our example.


First, we utilize the register method to register the necessary types, and then we
employ the resolve method to create the create_use_case instance.

container = Container()
container.register(BaseConnection, InMemoryConnection)
container.register(BaseRepository[Person], InMemoryPersonRepository)
container.register(BaseRepository[Order], InMemoryOrderRepository)
container.register(UnitOfWork)
container.register(CreatePersonAndOrderUseCase)

create_use_case = container.resolve(CreatePersonAndOrderUseCase)

new_person = Person(id=1, name="John Doe", age=30)


new_order = Order(id=1, order_date="2023-04-03", total_amount=100.0)

person, order = create_use_case.execute(new_person, new_order)


print(person, order)

Dependency Injector
The first DI framework we’ll explore is Dependency Injector. Dependency Injector
is a popular dependency injection framework specifically designed for Python
applications.

Before using Dependency Injector, you must install it using pip install

dependency_injector .

In the example below, we demonstrate how to use Dependency Injector. When


utilizing this framework, you must create a container class and register your types.

class Container(containers.DeclarativeContainer):
connection = providers.Singleton(
InMemoryConnection
)

person_repository = providers.Singleton(
InMemoryPersonRepository
)
order_repository = providers.Singleton(
InMemoryOrderRepository
)

unit_of_work = providers.Singleton(
UnitOfWork,
connection=connection,
person_repository=person_repository,
order_repository=order_repository
)

create_use_case = providers.Factory(
CreatePersonAndOrderUseCase,
unit_of_work=unit_of_work
)

if __name__ == '__main__':
container = Container()
create_use_case = container.create_use_case()

new_person = Person(id=1, name="John Doe", age=30)


new_order = Order(id=1, order_date="2023-04-03", total_amount=100.0)

person, order = create_use_case.execute(new_person, new_order)


print(person, order)

In this code, we create a Container class that inherits from declarativeContainer in


the dependency_injector library. We then define providers for each class that need to
be injected (i.e. InMemoryPersonRepository , InMemoryOrderRepository ,

InMemoryConnection , UnitOfWork , and CreatePersonAndOrderUseCase ).

We also specify the dependencies for the UnitOfWork and


CreatePersonAndOrderUseCase providers by passing in the connection ,

person_repository , and order_repository providers.

It’s important to note that this example only scratches the surface of what’s possible
with Dependency Injector.

The framework provides many features, including advanced scoping, lazy


evaluation, and even support for integrating with popular web frameworks like
Flask and FastAPI.

By exploring these capabilities, you can further enhance and streamline your
dependency management process, enabling more efficient and maintainable code
in your Python projects.

For more information, see the Dependency Injector documentation

Injector
Injector is another popular Python dependency injection framework. It uses
annotations, type hints, and providers to wire dependencies and manage object
lifetimes.

If you look at the example below, it uses the Injector framework to implement the
same example. Injector uses decorators like @provider and @singleton to register
the types.

class AppModule(Module):
@singleton
@provider
def provide_connection(self) -> InMemoryConnection:
return InMemoryConnection()

@singleton
@provider
def provide_person_repository(self) -> InMemoryPersonRepository:
return InMemoryPersonRepository()

@singleton
@provider
def provide_order_repository(self) -> InMemoryOrderRepository:
return InMemoryOrderRepository()

@inject
@singleton
@provider
def provide_unit_of_work(self,
connection: InMemoryConnection,
person_repository: InMemoryPersonRepository,
order_repository: InMemoryOrderRepository) -> Unit
return UnitOfWork(connection, person_repository, order_repository)

@inject
@singleton
@provider
def provide_create_use_case(self, unit_of_work: UnitOfWork) -> CreatePerson
return CreatePersonAndOrderUseCase(unit_of_work)
injector = Injector(AppModule())
create_use_case = injector.get(CreatePersonAndOrderUseCase)

new_person = Person(id=1, name="John Doe", age=30)


new_order = Order(id=1, order_date="2023-04-03", total_amount=100.0)

person, order = create_use_case.execute(new_person, new_order)


print(person, order)

Like with the Dependency Injector, it’s important to note that this example only
scratches what’s possible with Injector.

The framework provides many features, it automatically and transitively provides


dependencies for you. As an added benefit, Injector encourages nicely
compartmentalized code by using :ref:modules <module> .

For more information, see the documentation of Injector.

Comparing DI Frameworks
All three solutions — Custom Container, Dependency Injector, and Injector can
manage dependencies between components in a project.

Although their approach, features, and complexity differ, the underlying pattern
remains consistent across all of them. In each framework, the process begins with
registering the classes, followed by requesting the instances.

This fundamental pattern ensures a more organized and efficient management of


dependencies, regardless of the specific DI framework used.

Custom Container
A custom container is a hand-written dependency injection container tailored for
your project. It typically involves writing a container class that manages object
creation, lifecycle, and dependency resolution. Using a custom container gives you
full control over the implementation and lets you decide how to handle specific use
cases. However, it might need more robustness and features of a dedicated
dependency injection library.
Pros:

Full control over the implementation.

Straightforward and easy to understand.

Cons:

Limited in features compared to dedicated libraries.

Requires manual implementation for complex scenarios.

Can become difficult to maintain as the project grows.


Dependency Injector
Dependency Injector is a more feature-rich and complex dependency injection
library. It offers various features such as configuration management, container
lifecycle management, support for asynchronous injection, and more. It provides a
more powerful and flexible solution to managing dependencies in a project.

Pros:

An extensive set of features.

Supports different types of injections (constructor, attribute, method).

Supports asynchronous injections.

Configuration management built-in.

Well-documented with examples.

Cons:

Higher learning curve compared to other solutions.

More complex and can be overkill for smaller projects.

Injector
Injector is a lightweight and easy-to-use dependency injection library inspired by
Guice (a Java dependency injection library). It focuses on simplicity, which makes it
a good choice for small to medium-sized projects. Injector provides a
straightforward way to handle dependency injection without the complexity of
more feature-rich libraries.
Pros:

Easy to learn and use.

Lightweight, suitable for small to medium-sized projects.

Provides basic features required for dependency injection.

Cons:

Less feature-rich compared to Dependency Injector.

Lacks some advanced features like built-in configuration management.

In summary, a Custom Container is best for small projects where you prefer
simplicity and full control over the implementation. Injector is a good choice for
small to medium-sized projects that need a lightweight and easy-to-use dependency
injection library. Dependency Injector is ideal for larger projects that require a
more feature-rich and powerful dependency injection solution with support for
advanced use cases.

Conclusion
This article is the second piece in a series dedicated to exploring various proven
architectural design patterns in Python aimed at managing complexity.

This article examined Dependency injection in Python, a powerful software design


pattern that promotes loose coupling, modularity, and application testability

We provided an overview of dependency injection in Python, starting with the


basics, followed by the constructor and setter injection examples.

Furthermore, the article explored using different dependency injection frameworks


in Python, such as a Custom Container, Dependency Injector, and Injector.

Each framework has unique strengths and trade-offs, making them suitable for
different project requirements and complexity levels. From simple custom
containers for small projects to more feature-rich libraries like Dependency
Injector for larger projects.

As dependency management is crucial in building maintainable and scalable


software, understanding and implementing dependency injection can significantly
improve your Python applications’ overall quality and robustness.

By leveraging the right tools and techniques, developers can create flexible,
testable, and modular code, ultimately leading to better software design and
architecture.

All the code examples discussed in this article are within the accompanying GitHub
repository.

Python Python Programming Domain Driven Design Dependency Injection

Software Architecture

Follow

Written by Patrick Kalkman


2.5K Followers · Writer for ITNEXT

Dev & writer exploring open-source innovation & agile. Passionate about learning & teaching.
https://medium.com/@pkalkman/membership

More from Patrick Kalkman and ITNEXT

You might also like