Open In App

What is a Pythonic Way for Dependency Injection?

Last Updated : 15 Jul, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

Design pattern Dependency Injection (DI) achieves Inversion of Control (IoC) between classes and their dependencies. DI increases testability and modularity by externalizing the building and control of dependencies. This post will investigate DI from a Pythonic standpoint with an eye toward best practices and pragmatic solutions.

Understanding Dependency Injection:

Dependency Injection involves injecting a class's dependencies rather than letting the class generate them on its own. This facilitates easy code management and testing and loose coupling.

Benefits of Dependency Injection:

  • Modularity: Separates the building of dependencies from their use, therefore encouraging a modular codebase.
  • Testability: Testability lets one easily substitute dependencies with mocks or stubs, therefore facilitating testing.
  • Maintainability: By lowering close dependency between components, maintainability of the code simplifies upgrading and maintenance of it.

Principles of Pythonic Dependency Injection

Pythonic Design Patterns

Pythonic design patterns stress simplicity, readability, and the application of built-in language tools. In the context of DI, this implies using Python's dynamic character and simple syntax to apply DI neatly and effectively.

Writing Clean and Readable Code

Follow these ideas to guarantee DI implementations are Pythonic:

  • Explicit is superior to implicit: Specify exactly where and how dependencies are introduced.
  • Simple is better than complicated. Steer clear of too ambitious DI solutions.
  • Count of readable words: Create easily readable and understandable codes for others.

Dependency Injection Techniques in Python

Constructor Injection

Constructor Injection is supplying dependencies via a class's initializer, __init__ method.

Python
class Service:
    def do_something(self):
        print("Service is doing something")

class Client:
    def __init__(self, service: Service):
        self.service = service

    def perform_task(self):
        self.service.do_something()

# Dependency Injection
service = Service()
client = Client(service)
client.perform_task()

Output
Service is doing something

Setter Injection

Setter Injection lets dependencies be injected via a process following object creation.

Python
class Service:
    def do_something(self):
        print("Service is doing something")

class Client:
    def __init__(self):
        self.service = None

    def set_service(self, service: Service):
        self.service = service

    def perform_task(self):
        self.service.do_something()

# Dependency Injection
service = Service()
client = Client()
client.set_service(service)
client.perform_task()

Output
Service is doing something

Method Injection

Method injection is forwarding dependencies straight to the required method.

Python
class Service:
    def do_something(self):
        print("Service is doing something")

class Client:
    def perform_task(self, service: Service):
        service.do_something()

# Dependency Injection
service = Service()
client = Client()
client.perform_task(service)

Output
Service is doing something

Using Python Libraries for Dependency Injection

Overview of Popular Libraries

Each providing unique features and capabilities, several libraries can assist control dependency injection in Python programmes.

Dependency Injector

Popular library Dependency Injector gives a complete framework for DI in Python.

Python
pip install dependency-injector


from dependency_injector import containers, providers

class Service:
    def do_something(self):
        print("Service is doing something")

class Client:
    def __init__(self, service: Service):
        self.service = service

    def perform_task(self):
        self.service.do_something()

class Container(containers.DeclarativeContainer):
    service = providers.Singleton(Service)
    client = providers.Factory(Client, service=service)

container = Container()
client = container.client()
client.perform_task()
Service is doing something

Flask-Injector

By combining DI with the Flask web platform, Flask-Injector simplifies web application dependability management.

Python
pip install flask flask-injector


from flask import Flask
from flask_injector import FlaskInjector
from injector import inject, singleton

class Service:
    def do_something(self):
        print("Service is doing something")

class Client:
    @inject
    def __init__(self, service: Service):
        self.service = service

    def perform_task(self):
        self.service.do_something()

def configure(binder):
    binder.bind(Service, to=Service, scope=singleton)

app = Flask(__name__)

@app.route('/')
def index(client: Client):
    client.perform_task()
    return 'Task performed'

FlaskInjector(app=app, modules=[configure])

if __name__ == '__main__':
    app.run()
Service is doing something

Wiring with Pydantic

Dependency wiring among other configuration validation and management tools may be found in Pydantic.

Python
!pip install pydantic

# Writing the .env file
with open('.env', 'w') as f:
    f.write('db_url=your_database_url\n')
    f.write('api_key=your_api_key\n')

    
from pydantic import BaseSettings

class Settings(BaseSettings):
    db_url: str
    api_key: str

# Load settings from the .env file
settings = Settings(_env_file='.env')

# Print the settings
print(settings.db_url)
print(settings.api_key)
your_database_url
your_api_key

Implementing Dependency Injection in Python

Step-by-step execution:

  • First define the services and their interfaces to then define the dependencies.
  • Apply the DI Container method. Design a container class to handle dependant wiring and instantiation.
  • Dependencies on injections: Add dependencies into the client classes via constructor, setter, or method injection.

Practical Examples and Use Cases

Example-1: Web Application

Python
class Database:
    def connect(self):
        print("Connecting to the database...")

class Service:
    def __init__(self, database: Database):
        self.database = database

    def execute(self):
        self.database.connect()
        print("Service is executing...")

class Application:
    def __init__(self, service: Service):
        self.service = service

    def run(self):
        self.service.execute()

# Dependency Injection
database = Database()
service = Service(database)
app = Application(service)
app.run()

Output
Connecting to the database...
Service is executing...

Example 2: CLI Tool

Python
class Logger:
    def log(self, message):
        print(message)

class Task:
    def __init__(self, logger: Logger):
        self.logger = logger

    def run(self):
        self.logger.log("Task is running")

def main():
    logger = Logger()
    task = Task(logger)
    task.run()

if __name__ == '__main__':
    main()

Output
Task is running

Conclusion

Powerful for encouraging modularity, testability, and maintainability in your Python code is dependency injection. Using suitable libraries and applying Pythonic ideas will help you to develop DI in a neat and effective way.


Next Article
Article Tags :
Practice Tags :

Similar Reads