Course
Dependency injection is a design pattern where objects or services (we mean dependencies here) are provided to a class from the outside, instead of the class creating them internally. This decouples components.
Using dependency injection in Python has many benefits, such as:
- Modularity: Your code is split into smaller, reusable parts.
- Testability: It is easier to test your code because you can swap out the real part with mock ones.
- Maintainability: You can update or modify parts of your code without breaking the rest of the system.
In this tutorial, I will demonstrate how to use dependency injection in Python through simple, practical examples.
Understanding Dependency Injection in Python
To understand how to implement dependency injection, it's essential that you grasp the key principles that govern it.
Dependency injection is based on something called the principle of Inversion of Control (IoC), which means that instead of a class creating and managing its dependencies, it receives them from an external source. This helps to focus on a single task and makes your code clean.
Injecting dependencies in Python includes these common way,s although there are others:
- Constructor injection: Pass dependencies through the class constructor.
- Setter injection: Set dependencies using a setter method after the object is created.
- Method injection: Pass dependencies directly to the method that needs them.
Why Use Dependency Injection in Python?
As a developer, understanding and implementing dependency injection can make a lot of difference in your code design. Let’s look at some benefits of dependency injection.
Modularity, resusability, and flexibility
Dependency injection enables you to break down your code into smaller, more focused components, allowing for greater flexibility and modularity.
Each part or component handles a specific task and relies on external dependencies, making it easier to reuse in different parts of your or someone else’s application
For example, if you have a class that sends messages. Instead of building an email or SMS logic directly into your service, you can inject an email or sms sender class.
Now, your class that sends messages does not care how the message is sent, but just uses the sender provided to it. This implies that you can use your messaging sender with different email or sms senders.
Maintainability and testability
When using dependency injection, you can easily replace real dependencies with mock data during testing. This makes it simpler to write unit tests without relying on APIs or databases.
Suppose, as a thinking exercise, that you have a class that saves data to a database. For testing purposes, you can inject a mock database instead of a real one to avoid coupling your test to the production database.
Dependency injection enables you to modify specific parts of your code without breaking the entire system. This is particularly handy in situations where you need to switch payment providers in an application, such as from Stripe to PayPal, without modifying the rest of your code.
Inversion of control and swappable dependencies
The idea of of inversion of control is this: dependency injection will shift the responsibility of creating and managing dependencies to an external source.
This implies that you can easily swap one implementation for another; for example, if you have a ReportGenerator class that formats and exports reports.
Instead of the class deciding on the format to export, you could inject an exporter class, such as PDFExporter, ExcelExporter, or CSVExporter, into it.
The ReportGenerator class does not care how the export happens, so to change a format, you just swap the exporter you inject, without modifying the ReportGenerator at all.
Implementing Dependency Injection in Python
Now that you have a clear glimpse of what dependency injection is and why it’s useful, let’s look at how you can implement it in Python. Let’s start with the simplest method: manual dependency injection.
Manual dependency injection
In manual dependency injection, you create dependencies yourself and pass them into the class or function that needs them.
This method works well for small applications, but as your application scales, you need to use a dependency injection framework to avoid errors.
Here is an example of code without dependency injection
class EmailService:
def send_email(self, message):
print(f"Sending email: {message}")
class UserNotifier:
def __init__(self):
self.email_service = EmailService() # Creates its own dependency
def notify(self, message):
self.email_service.send_email(message)
notifier = UserNotifier()
notifier.notify("Welcome!")
In the above example, UserNotifier is tightly coupled to EmailService. You can’t easily swap or mock EmailService for testing.
Here is another version, but with manual dependency injection.
class EmailService:
def send_email(self, message):
print(f"Sending email: {message}")
class UserNotifier:
def __init__(self, email_service):
self.email_service = email_service # Dependency is injected
def notify(self, message):
self.email_service.send_email(message)
email_service = EmailService()
notifier = UserNotifier(email_service)
notifier.notify("Welcome!")
The version above is flexible and allows you to inject various messaging services; you can also replace EmailService with a mock in tests.
Using the dependency-injector framework
As your application grows, it becomes complex and messy to manage dependencies.
The dependency-injector framework provides a structured approach.
Container-provider architecture
The dependency-injector framework works based on a container-provider architecture.
- Container: This is the central registry for your application dependencies.
- Providers: This defines how your dependencies are created.
- Configuration: This allows you to modify settings and inject them into objects.
- Overriding: Let’s you replace dependencies without changing application code, especially during testing.
from dependency_injector import containers, providers
# Services
class EmailService:
def send_email(self, message):
print(f"Sending email: {message}")
class UserNotifier:
def __init__(self, email_service):
self.email_service = email_service
def notify(self, message):
self.email_service.send_email(message)
# Container
class Container(containers.DeclarativeContainer):
email_service = providers.Singleton(EmailService)
user_notifier = providers.Factory(UserNotifier, email_service=email_service)
# Usage
container = Container()
notifier = container.user_notifier()
notifier.notify("Hello!")
In the example above:
-
Containerdefines how instances are created -
Singletonensures only one instance ofEmailServiceexists -
Factorycreates a newUserNotifierwith theEmailServiceinjected automatically.
Wiring mechanism
You can use decorators like @inject to reduce the need for manual dependency passing.
Here is another version of the previous example:
from dependency_injector import containers, providers
# Define services
class EmailService:
def send_email(self, message):
print(f"Sending email: {message}")
class UserNotifier:
def __init__(self, email_service: EmailService):
self.email_service = email_service
def notify(self, message):
self.email_service.send_email(message)
# Create container
class Container(containers.DeclarativeContainer):
email_service = providers.Singleton(EmailService)
user_notifier = providers.Factory(UserNotifier, email_service=email_service)
from dependency_injector.wiring import inject, Provide
@inject
def main(notifier: UserNotifier = Provide[Container.user_notifier]):
notifier.notify("Hello!")
if __name__ == "__main__":
container = Container()
container.wire(modules=[__name__]) # This wires the current module
main()
In the current version;
-
Without manually adding the dependency, the
@injectdecorator automatically injects the function or class dependency. -
The
Provideclass tells thedependency-injectorwhich dependency to inject.
This approach eliminates the need for manual wiring dependencies from the container.
It’s helpful in larger applications where services may depend on multiple components (hence this need to initialize them automatically).
Using Dependency Injection in Popular Python Frameworks
Most modern Python frameworks make it easy to work with dependency injection, allowing you to write cleaner and more testable code.
In this section, you will learn how dependency injection is applied in Flask, Django, and FastAPI.
Dependency injection in Flask
Flask does not have built-in support for dependency injection, but you can implement it using the dependency-injector library through these following steps.
Step 1: Define your services
class GreetingService:
def get_greeting(self, name):
return f"Hello, {name}!"
Step 2: Set up your dependency injection container
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
greeting_service = providers.Factory(GreetingService)
Step 3: Create the Flask application and inject dependencies.
from flask import Flask, request
from dependency_injector.wiring import inject, Provide
container = Container()
app = Flask(__name__)
@app.route("/greet")
@inject
def greet(greeting_service: GreetingService = Provide[Container.greeting_service]):
name = request.args.get("name", "World")
return greeting_service.get_greeting(name)
container.wire(modules=[__name__])
if __name__ == "__main__":
app.run(debug=True)
From the code above:
-
The function,
name = request.args.get(“name”, “World”)extracts thenamefrom the query string, and defaults to”World”if not provided. -
return greeting_service.get_greeting(name)calls the method on the injected service and returns a greeting message. -
container.wire(modules=[__name__])tells the dependency injector to scan this module for@injectdecorators andProvide[...]markers to connect them to the actual providers.
Step 4: Test the application
Run the application on your terminal using the command python <name_of_file>.py, and visit the URL http://localhost:5000/greet?name=Jacob.
You should see the following.

Dependency injection in Django
Just like Flask, Django does not offer built-in dependency injection. But, you can use its class-based views, middleware, and app structure to integrate Dependency injection manually or using the dependency-injector library.
Here is a step-by-step procedure for implementing dependency injection in Django.
Step 1: Define your service
Create a simple service containing your business logic in a new file services.py under your app folder.
# myapp/services.py
class GreetingService:
def get_greeting(self, name):
return f"Hello, {name}!"
Step 2: Set up the dependency injector container
Inside your application directory, create a container using the dependency-injector library inside a new file containers.py.
# myapp/containers.py
from dependency_injector import containers, providers
from .services import GreetingService
class Container(containers.DeclarativeContainer):
greeting_service = providers.Factory(GreetingService)
Step 3: Create a View with injected dependencies
Use the @inject decorator in your view to receive dependencies in views.py.
# myapp/views.py
from django.http import HttpResponse
from dependency_injector.wiring import inject, Provide
from .containers import Container
from .services import GreetingService
@inject
def greet_view(
request, greeting_service: GreetingService = Provide[Container.greeting_service]
):
name = request.GET.get("name", "World")
message = greeting_service.get_greeting(name)
return HttpResponse(message)
Step 4: Update urls.py
Under your project folder, go to the urls.py file and hook your dependency injector into Django’s URL routing system.
from django.contrib import admin
from django.urls import path
from myapp.views import greet_view
urlpatterns = [
path("admin/", admin.site.urls),
path("greet/", greet_view),
]
Step 5: Wire the container in apps.py
Go to apps.py and wire the container to the application.
# myapp/apps.py
from django.apps import AppConfig
from .containers import Container
class MyAppConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "myapp"
def ready(self):
container = Container()
container.wire(modules=["myapp.views"])
Then go to your app config in INSTALLED_APPS and add your application.
# settings.py
INSTALLED_APPS = [
'myapp.apps.MyAppConfig',
...
]
Step 6: Test the View
Start your Django server and access the view through the URL http://localhost:8000/greet/?name=Django.
You should see the following.

Dependency Injection in FastAPI
FastAPI has built-in support for dependency injection through the special marker Depends() that injects whatever a function returns.
Here is a step-by-step guide on how to go about FastAPI dependency injection.
Step 1: Define a service
class GreetingService:
def get_greeting(self, name: str) -> str:
return f"Hello, {name}!"
Step 2: Create the dependency function
def get_greeting_service():
return GreetingService()
Step 3: Set up the FastAPI app and use Dependency Injection
app = FastAPI()
@app.get("/greet")
def greet(
name: str = "World", service: GreetingService = Depends(get_greeting_service)
):
return {"message": service.get_greeting(name)}
Step 4: Run and test the application
Start your server using the command uvicorn main:app –reload and visit the URL http://localhost:8000/greet?name=FastAPI. You should see the following response.

Comparing Python Dependency Injector Frameworks
There are various Python Dependency frameworks, each with its own strengths, syntax, and use cases. Choosing the proper framework depends on the size and structure of your project.
Here is a comparison of some common Python dependency injection frameworks.
|
Framework |
Best For |
Strengths |
Limitations |
|
|
Complex applications in production use |
Full-featured, fast, and configurable |
Verbose |
|
Simple and suitable for minimal applications |
Clean syntax |
Less advanced than others. |
|
|
Advanced binding logic |
Auto-wiring |
Slower due to introspection |
|
|
Simple applications |
Minimal and fast |
Limited features compared to others |
|
|
FastAPI applications |
Built-in support for async, and is also testable |
Not reusable outside FastAPI |
|
|
Flask applications |
Easy integration with Flask applications |
Depends on |
|
|
Django projects |
Provides dependency injection for views and middleware |
Depends on |
Advanced Implementation Patterns
As your application code base becomes large, dependency injection becomes challenging to maintain.
You can use advanced dependency injection to handle tasks such as request-scoped resources, cleanup, and separation of concerns.
Scoped dependencies
Scoped dependencies enable you to control how long a dependency persists, for example, when you want a new object per request, a shared object across the app's lifetime, or automatic cleanup of resources such as database connections.
Most dependency injector frameworks support scopes like:
- Singleton: One instance for the entire app.
- Factory: A new instance every time it is injected.
- Thread-local or request: A single instance per thread/request.
- Resource: Managed with setup and teardown logic.
The following is an example of scoped dependencies with nested containers using dependency-injector.
from dependency_injector import containers, providers, resources
# A simple resource that needs setup and cleanup
class MyResource:
def __enter__(self):
print("Setting up the resource")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("Cleaning up the resource")
# Create a container to manage the resource
class MyContainer(containers.DeclarativeContainer):
resource = providers.Resource(MyResource)
# Using the resource with a context manager
container = MyContainer()
with container.resource() as r:
print("Using the resource")
In the example above, MyResource mimics a class that requires setup and teardown, such as opening/closing a file or database.
The container provides the resource using providers.Resource, and when you use with container.resource(), it enters and exits the context correctly.
This pattern ensures that resources, such as file handles, database connections, or network sessions, are managed cleanly.
Imagine an API endpoint that needs a database session for the current request or a logger with request-specific context.
With scoped dependencies, the framework creates a request-scoped container when the request begins resolving all dependencies from that scope.
When the request ends, any necessary cleanup, such as closing database sessions, occurs automatically. This ensures that states are not shared across requests and avoids resource leaks.
Asynchronous injection
To handle I/O bound tasks efficiently, you need asynchronous execution. Dependency injection systems in frameworks like FastAPI and dependency-injector, offer asynchronous injection using async/await.
Asynchronous injection refers to injecting dependencies that are async def functions or managing async resources like async database connections, external APIs, or background tasks.
Rather than resolving them synchronously, the dependency injection system waits for them to be resolved before injecting.
The following is an example of an async dependency in FastAPI:
from fastapi import FastAPI, Depends
app = FastAPI()
# Async dependency
async def get_user():
# Simulate async I/O
await some_async_db_query()
return {"name": "Philip Jones"}
@app.get("/profile")
async def read_profile(user: dict = Depends(get_user)):
return {"user": user}
FastAPI automatically handles the async resolution of get_user and injects the result.
dependency-injector also supports async lifecycle management using AsyncResource providers.
from dependency_injector import containers, providers, resources
class AsyncDB:
async def connect(self):
print("Connected")
return self
async def disconnect(self):
print("Disconnected")
class AsyncDBResource(resources.AsyncResource):
async def init(self) -> AsyncDB:
db = AsyncDB()
return await db.connect()
async def shutdown(self, db: AsyncDB):
await db.disconnect()
class Container(containers.DeclarativeContainer):
db = providers.Resource(AsyncDBResource)
# Usage
async def main():
container = Container()
await container.init_resources()
db = await container.db() # Get the AsyncDB instance
# Use db here...
await container.shutdown_resources()
In the code above:
-
resources.AsyncResourceis used for async setup/teardown logic. -
await container.init_resources()initializes all resources and callsinit. -
await container.shutdown_resources()cleans up all resources. -
await container.db()gets the actual resources instance, which isAsyncDB.
Using async dependency injection helps keep your application performant by avoiding synchronous I/O in asynchronous routes, and eliminates the need to manually await dependencies in your logic.
Security Implications
While dependency injection improves modularity and testability, it can introduce potential security risks if not managed carefully.
One significant risk is dependency confusion, where malicious packages mimic legitimate ones. For example, attackers have uploaded packages to PyPI with names used in internal enterprise systems to trick apps into using the malicious version.
To stay secure, ensure that you never inject untrusted or user-controlled components and validate configuration sources, such as environment variables, before they are injected.
Use requirements.txt or poetry.lock files to lock exact versions, and automate security scanning on dependency changes. Additionally, ensure that you regularly audit your dependencies for vulnerabilities.
Centralizing control of your dependencies is also essential, as it ensures consistency across your applications and facilitates the application of security patches or version updates more easily. It helps avoid "shadow dependencies" injected in different ways across the codebase.
Several tools can detect risks and improve your security posture:
- Snyk: Scans for known vulnerabilities in Python packages and suggests fixes.
- Xygeni: Offers protection for CI/CD pipelines.
- Safety: Checks for insecure packages and code vulnerabilities.
- Bandit: A Static code analyzer for Python focused on security issues.
Real-World Applications
Dependency injection is not just a theoretical pattern; it is used in various production systems to manage complexity, improve flexibility, and streamline testing.
Web service configuration
In modern web applications, dependency injection plays a crucial role in managing standard services, such as authentication, logging, and database access.
Frameworks like FastAPI play an essential role in resolving routes and dependencies.
The example below illustrates a centralized authentication implementation in FastAPI.
from fastapi import Depends, HTTPException
def get_current_user(token: str = Depends(oauth2_scheme)):
user = verify_token(token)
if not user:
raise HTTPException(status_code=401)
return user
@app.get("/protected")
def protected_route(user: dict = Depends(get_current_user)):
return {"message": f"Hello {user['name']}"}
In the code above, get_current_user() is injected as a route dependency, but the logic for checking the token is centralized and reusable.
You can also do the same for database access, such that database sessions are automatically managed and cleaned.
Here is an example of how you can use dependency injection to centralize database access.
from sqlalchemy.ext.asyncio import AsyncSession
async def get_db_session() -> AsyncSession:
async with async_session() as session:
yield session
@app.get("/items")
async def read_items(db: AsyncSession = Depends(get_db_session)):
return await db.execute("SELECT * FROM items")
In the code above, the get_db_session is a dependency provider. When the read_items route is called, FastAPI automatically injects the db parameter, allowing the route to access the database without manually managing session setup or teardown.
Test-driven development
Dependency injection makes it easy for you to change the dependencies components receive, which is important when testing.
You can replace real services with mocks during testing, making your code more modular and easier to test.
FastAPI and dependency-injector support overriding dependencies at test time.
Here is an example in FastAPI.
from fastapi.testclient import TestClient
from main import app, get_db_session
def override_db():
return TestDBSession()
app.dependency_overrides[get_db_session] = override_db
client = TestClient(app)
response = client.get("/items")
assert response.status_code == 200
In the code example above, get_db_session is overridden with a mock database during tests; hence, there is no need to patch or alter the production code.
Same also in depedency-injector, you can override your real database with a mock database for testing purposes, like in the example below.
from containers import Container
def test_service_behavior():
container = Container()
container.db.override(providers.Factory(TestDB))
service = container.service()
assert service.get_data() == "mocked"
The override() method replaces the real dependency with a mock, which is clean, controlled, and reversible.
Here are some strategies you should use for dependency overrides:
- Constructor injection: Ensure you pass mocks directly into the constructor.
- Framework overrides: Use built-in override tools such as
FastAPI.dependency_overridesin FastAPI ordependency_injector.override()independency-injector. - Fixtures: Use testing frameworks like
pytestto create reusable mocks and inject them via fixtures.
Best Practices and Anti-Patterns
Although dependency injection is intended to make code modular and easy to test, it can lead to bugs or create complexity in the code.
Recommended practices
Here are some best practices you should adhere to when creating dependencies:
- Program to interfaces, not implementations: Define your dependencies using abstract base classes or interfaces rather than complex implementations. This makes your code easier to test and also encourages the use of mocks in unit tests.
- Favor composition over inheritance: Rather than building complex class hierarchies, compose objects using injected dependencies to keep components small and focused.
- Centralized dependency configuration: Utilize a single container or module to define and manage dependencies, making overriding and testing easier.
- Avoid circular dependencies: Carefully design your dependency graph to prevent circular references by using factory functions or providers to delay instantiation.
Common pitfalls
Here are some common pitfalls you should avoid when using dependency injection.
- Service locator anti-pattern: Avoid using a global container to dynamically fetch dependencies, as this can conceal dependencies and make code complicated to understand and test.
- Over-injection: Avoid injecting too many services into a single class or function to prevent your code from leading to bloated constructors and hard-to-test logic.
- Scope mismanagement and state leakage: Ensure you always use the right scope to avoid sharing state across users or tests.
Dependency Injection Testing in Python
Dependency injection makes it easy to inject mock objects into your components, thereby simplifying unit tests. This allows you to separate fundamental components from test-specific ones.
With dependency injection, tests are deterministic, have no side effects, and result in faster test execution.
Here is an example showing how you can use dependency Injection to inject mock, stub, or fake versions of those dependencies in your tests.
class EmailService:
def send_email(self, to: str, message: str): ...
class Notifier:
def __init__(self, email_service: EmailService):
self.email_service = email_service
def alert(self, user_email):
self.email_service.send_email(user_email, "Alert!")
In tests, replace EmailService with a mock:
class TestEmailService:
def send_email(self, to, message):
self.sent_to = to
def test_notifier_sends_email():
test_email = TestEmailService()
notifier = Notifier(test_email)
notifier.alert("test@example.com")
assert test_email.sent_to == "test@example.com"
Ensure to use dependency injection often and avoid monkey patching for maintainability and avoid breaking changes when internal name changes.
Conclusion
Although you might not need dependency injection initially for small projects, as the project grows and scales, you can benefit from the structure and modularity offered by dependency injection.
Looking ahead, the future of dependency injection in Python is promising, and we can expect it to continue with an improved type system and enhanced async lifecycle management.
Mastering dependency injection is essential for becoming a fluent Python developer. But it doesn’t stop there; many other advanced concepts can elevate your Python skills.
To help you accelerate your learning journey, here are some of our resources you should look at.
Courses:
- Developing Python Packages
- Introduction to PySpark
- Object-Oriented Programming in Python
- Intermediate Object-Oriented Programming in Python
Blog Posts:
Experienced data science instructor and Biostatistician with expertise in Python, R, and machine learning.
FAQs
What is dependency Injection?
Dependency injection is a design pattern that enables you to write cleaner code by creating dependencies within a class, which can be passed from outside, making your code easier to manage and modify.
What are the main benefits of dependency Injection?
Dependency injection makes your code modular, testable, and maintainable.
How can I implement dependency injection in Python?
You can implement dependency injection in Python either manually or by using libraries such as dependency_injector.
What is asynchronous Injection?
Asynchronous injection refers to injecting dependencies that are async def functions or managing async resources like async database connections, external APIs, or background tasks.
What are the security risks of dependency injection, and how can I handle them?
One significant risk is dependency confusion, where malicious packages mimic legitimate ones. To prevent this, you can use tools like Snyk, Xygeni, Safety, or Bandit to scan your code and packages for vulnerabilities.

