Sun Nov 26 2023
Understanding Python Dependency Injection and How Does It Work?
Dependency Injection (DI) is a powerful design pattern in software development that enhances code maintainability, testability, and scalability. This technique defining the dependencies among objects. In Python, dependency injection enables loosely coupled components and facilitates more modular and flexible applications. Let's dive into the concepts and workings of dependency injection in Python.
What is Dependency Injection?
Dependency Injection is a technique where dependencies of a class or function are injected from the outside, rather than the class/function creating its dependencies internally. This practice allows for more modular and reusable code by decoupling components and making them easier to test and maintain.
There are various classes and objects defined when writing code. Most of the time, these classes depend on other classes in order to fulfill their intended purpose. These classes or a better word might be Components, know the resources they need and how to get them. DI handles defining these dependent resources and provides ways to instantiate or create them externally. Dependency Containers are used to implement this behavior and holds the map of dependencies for the components.
Originally, the dependency injection pattern got popular in languages with static typing, like Java. Dependency injection framework can significantly improve the flexibility of the language with static typing.
How Dependency Injection Works in Python
1. Constructor Injection
In Python, one of the common ways to perform dependency injection is through constructor injection. Dependencies are passed to a class's constructor as arguments.
class Example:
def __init__(self, dependency):
self.dependency = dependency
2. Setter Injection
Another approach is setter injection, where dependencies are set through setter methods.
class Example:
def set_dependency(self, dependency):
self.dependency = dependency
3. Injection Using Libraries
Python offers various libraries like injector, Django, Flask, or Guice that facilitate dependency injection through built-in functionalities or external modules.
Advantages of Dependency Injection
-
Dependency injection, as a software design pattern, has a number of advantages that are common for each language including Python:
-
Dependency Injection decreases coupling between a class and its dependency.
-
Dependency injection doesn't require any change in code behavior it can be applied to legacy code as a refactoring. The result is clients that are more independent and that are easier to unit test in isolation using stubs or mock objects that simulate other objects not under test. This ease of testing is often the first benefit noticed when using dependency injection.
-
Dependency injection can be used to externalize a system’s configuration details into configuration files allowing the system to be reconfigured without recompilation (rebuilding). Separate configurations can be written for different situations that require different implementations of components. This includes, but is not limited to, testing.
-
Reduction of boilerplate code in the application objects since all work to initialize or set up dependencies is handled by a provider component.
-
Dependency injection allows a client to remove all knowledge of a concrete implementation that it needs to use. This helps isolate the client from the impact of design changes and defects. It promotes re-usability, test-ability, and maintainability.
-
Dependency injection allows a client the flexibility of being configurable. Only the client’s behavior is fixed. The client may act on anything that supports the intrinsic interface the client expects.
While Python is a very flexible interpreted language with dynamic typing, there is a meaning that dependency injection doesn’t work for it as well, as it does for Java. Dependency Injection in Python is little different. Python has a micro framework library for DI, called dependency_injector. It was designed to be a unified, developer-friendly tool that helps to implement dependency injection design pattern in - formal, pretty, Pythonic way.
Common Use Cases
- Web Development: Dependency injection is often used in web frameworks like Flask or Django for managing services, databases, or external APIs.
- Unit Testing: DI simplifies mocking dependencies during unit testing, allowing for more focused and isolated tests.
Let's See, How Does It Work?
The following example demonstrates the usage and implementation of DI in Python.
-
At first, create a file named email_client.py containing the EmailClient class which depends on the object.
-
Then, create a new file named e mail_reader.py which contains the EmailReader class and depends on the EmailClient object.
-
Now to define these dependencies externally, create a new containers.py file. Import the dependency_injector package and the classes to be used in DI.
-
Next, add the class Configs to the file. This class is a container with a configuration provider which provides all the configuration objects.
-
After that, add another class, Clients. This class is a container defining all kinds of clients. EmailClient is created with a singleton provider, asserting single instances of this class, and defining its dependency on an object.
-
The third container is the Readersclass, defining the dependency of EmailReader class on the EmailClient class.
-
To run the example, create the main.py file.
-
In the main.py file, the object is overridden with a given dictionary object. The EmailReader class was instantiated without instantiating the EmailClient class in the main file, removing the overhead of importing or creating it. That part is taken care by containers file.
Conclusion
Dependency Injection in Python is a powerful technique that enhances code modularity, testability, and maintainability. By allowing external control over a component's dependencies, it enables more flexible and scalable applications, facilitating a smoother software development process.