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.

pythonarchitecturepatterns

See Also