Python Dependency Injection — Core Concepts
What Is Dependency Injection?
Dependency injection (DI) is a design pattern where an object receives its dependencies from the outside rather than creating them internally. The “injection” is the act of passing in what a component needs.
In Python, DI is simpler than in languages like Java because Python’s dynamic typing and first-class functions give you natural injection points without heavy frameworks.
The Three Forms
Constructor Injection
Pass dependencies when creating an object:
class OrderService:
def __init__(self, db, email_sender):
self.db = db
self.email_sender = email_sender
The caller decides which database and email sender to use. The OrderService just uses what it receives. This is the most common and recommended form because dependencies are visible and required.
Function Parameter Injection
Pass dependencies as function arguments:
def process_payment(amount, payment_gateway):
return payment_gateway.charge(amount)
Pythonic and lightweight. Perfect for standalone functions and scripts where creating classes adds unnecessary ceremony.
Setter/Property Injection
Set dependencies after creation:
service = OrderService()
service.db = get_database()
Less common and generally discouraged because the object exists in a partially-configured state between creation and injection. Use it only when dependencies are truly optional.
Why It Matters
Testability
Without DI, testing a function that sends emails means either actually sending emails or monkeypatching internal imports. With DI, you pass a fake:
class FakeEmailSender:
def __init__(self):
self.sent = []
def send(self, to, body):
self.sent.append((to, body))
# In your test
service = OrderService(db=fake_db, email_sender=FakeEmailSender())
service.place_order(item)
assert len(service.email_sender.sent) == 1
No mocking libraries needed. No import patching. Just pass in a fake.
Flexibility
The same OrderService works with PostgreSQL in production, SQLite in development, and an in-memory store in tests. The service code never changes — only what gets injected.
Explicit Dependencies
When a class lists its dependencies in __init__, you can instantly see what it needs. Hidden dependencies (like importing a global database module deep inside a method) make code harder to understand and maintain.
DI Without Frameworks
Python doesn’t need a DI framework for most projects. The language itself supports DI through default arguments, closures, and module-level configuration:
def create_app(config):
db = Database(config.db_url)
cache = Redis(config.redis_url)
user_service = UserService(db=db, cache=cache)
return App(user_service=user_service)
This “composition root” pattern wires everything together at startup. Each component receives its dependencies, and the wiring logic lives in one place.
When Frameworks Help
For large applications with dozens of services and complex dependency graphs, DI containers like dependency-injector or FastAPI’s built-in Depends system automate the wiring. They handle lifecycle management (singleton vs per-request) and make it easy to swap entire configuration profiles.
FastAPI’s approach is particularly elegant because it integrates DI into the route handler signature, making dependencies both declarative and automatically resolved.
Common Misconception
“DI means you need a framework.” In Python, DI is a pattern, not a library. Passing arguments to constructors is dependency injection. Most Python projects under 50 files do fine with manual wiring. Frameworks help at scale, but the pattern works without them.
The one thing to remember: Dependency injection makes dependencies visible and swappable by passing them in from the outside — in Python, this is often as simple as adding a parameter.
See Also
- Python Aggregate Pattern Why grouping related objects under a single gatekeeper prevents data chaos in your Python application.
- Python Bounded Contexts Why the same word means different things in different parts of your code — and why that is perfectly fine.
- Python Bulkhead Pattern Why smart Python apps put walls between their parts — like a ship that stays afloat even with a hole in the hull.
- Python Circuit Breaker Pattern How a circuit breaker saves your app from crashing — explained with a home electrical fuse analogy.
- Python Clean Architecture Why your Python app should look like an onion — and how that saves you from painful rewrites.