Filter concepts by levelShowing all levels.

Python · Dependency Injection

Dependency Injection

Depending on abstractions instead of concrete classes, passing dependencies in rather than building them, and why that is what makes a class testable — every example run for real, not just described

  • Dependency inversiondepend on an abstraction, not one concrete class
  • Constructor injection__init__(self, dependency) — passed in, not built inside
  • Containersa registry that builds and wires a whole dependency chain
  • Testing with fakesswap a real dependency for a fake with no class changes

1.Dependency inversion

Dependency inversion means a class depends on an abstraction (like an ABC with a send method) instead of one concrete class it builds itself. That abstraction is what makes swapping the real implementation for a test double possible.

Key features

  • High-level code should depend on an abstraction, not a concrete class it instantiates itself
  • In Python, an abc.ABC with @abstractmethod is a common way to express that abstraction explicitly
  • Duck typing means the abstraction doesn't even need a base class — any object with the right method works
  • Dependency injection is the technique; dependency inversion is the principle it exists to satisfy
  • The concrete class still gets built somewhere — inversion moves that decision out of the class that uses it
Depending on a concrete class vs. an abstraction

Tightly coupled

  • OrderService creates SmtpMailer itself
  • Swapping mailers means editing OrderService
  • A test cannot avoid sending real email

Depends on an abstraction

  • OrderService accepts anything shaped like Mailer
  • SmtpMailer, a queue-based mailer, or a fake all fit
  • A test passes a fake with no code changes
  • Tightly coupled
    • OrderService creates SmtpMailer itself
    • Swapping mailers means editing OrderService
    • A test cannot avoid sending real email
  • Depends on an abstraction
    • OrderService accepts anything shaped like Mailer
    • SmtpMailer, a queue-based mailer, or a fake all fit
    • A test passes a fake with no code changes

Command

python
from abc import ABC, abstractmethod

class Mailer(ABC):
    @abstractmethod
    def send(self, to, subject): ...

class OrderService:
    def __init__(self, mailer: Mailer):   # depends on the abstraction
        self.mailer = mailer

Depend on the abstraction (what a dependency must do), not the concrete class (how one particular version does it) — that is what makes swapping in a test double possible.

2.Constructor injection

Constructor injection passes a dependency into __init__ as a parameter, instead of the class creating it itself. UserService(repository) receives its Repository from the caller, so any object with the right methods can be passed in.

Key features

  • __init__(self, repository) accepts the dependency as a parameter, stored on self
  • The class never constructs its own dependency internally — the caller decides which one to pass
  • The dependency is fixed for the lifetime of that instance, set once at construction
  • Any object with matching methods works — a real Repository, a different implementation, or a test fake
  • This is the most common form of dependency injection in everyday Python code
The dependency is handed in, not built inside

Repository()

built by the caller

UserService(repository)

passed into __init__

self.repository

stored, used for the instance’s life

  1. Repository() — built by the caller
  2. UserService(repository) — passed into __init__
  3. self.repository — stored, used for the instance’s life

Command

python
class UserService:
    def __init__(self, repository):
        self.repository = repository   # injected, not constructed here

    def get_username(self, user_id):
        return self.repository.get_user(user_id)["name"]

Pass a dependency into __init__ as a parameter; do not build it with ClassName() inside the constructor — that one change is what makes swapping it in tests possible.

3.Function injection

Function injection passes a dependency straight into a function call rather than through a class. total_price(items, tax_calculator) takes the tax rule as an argument, so a different calculator can be passed without editing total_price.

Key features

  • A dependency passed as a function parameter is function injection — no class or constructor required
  • A default parameter value (sender=None, then falling back to a default) still allows overriding per call
  • Passing a function itself as an argument (tax_calculator) is a normal, direct form of this technique in Python
  • Useful for utilities and scripts where a full class feels heavier than the problem needs
  • The function only calls the dependency by its parameter name — it never hardcodes a specific implementation

Command

python
def total_price(items, tax_calculator):
    subtotal = sum(items)
    return subtotal + tax_calculator(subtotal)   # dependency passed in, not hardcoded

Pass a dependency as a function parameter — often a callable itself — rather than hardcoding which implementation the function uses inside its body.

4.Dependency containers

A dependency container is a registry that knows how to build each dependency and wires them together automatically. container.resolve("user_service") builds the Repository, then the UserService that needs it, without you writing that chain by hand.

Key features

  • A container maps a name to a factory function that knows how to build that dependency
  • container.resolve(name) builds the dependency and everything it depends on, recursively
  • A container commonly caches an instance after first building it, so resolve() returns the same object each time (a singleton)
  • Manual constructor injection still works underneath — the container is just automating the wiring, not replacing it
  • Large frameworks (FastAPI, Django REST Framework extensions, dependency-injector) ship their own container; small projects often hand-roll a dict-based one
Resolving one name builds its whole dependency chain

resolve("user_service")

build repository

build UserService(repository)

cache and return

  • resolve("user_service")
    • leads to build repository
  • build repository
    • leads to build UserService(repository)
  • build UserService(repository)
    • leads to cache and return
  • cache and return

Command

python
class Container:
    def __init__(self):
        self._singletons = {}
        self._factories = {
            "repository": lambda c: Repository(),
            "user_service": lambda c: UserService(c.resolve("repository")),
        }

    def resolve(self, name):
        if name not in self._singletons:
            self._singletons[name] = self._factories[name](self)
        return self._singletons[name]

A container maps names to factories and resolves a dependency chain automatically — reach for one once wiring by hand gets repetitive, not for a single dependency.

5.FastAPI dependency injection

FastAPI's Depends(get_db_session) marks a parameter as a dependency to resolve before the endpoint runs. FastAPI calls get_db_session for you and passes its return value in — the endpoint never calls it directly.

Key features

  • def endpoint(session=Depends(get_db_session)) tells FastAPI to call get_db_session first and pass its result as session
  • A dependency can itself depend on another Depends(...) parameter — FastAPI resolves the whole chain before the endpoint runs
  • This is function injection: the endpoint receives its dependency as a parameter, not by importing or constructing it
  • Swapping a dependency for tests uses app.dependency_overrides, which replaces one Depends target with another for the test client
  • FastAPI's actual resolver is more capable than the version here — it also handles caching per request and async dependencies

Command

python
def get_db_session():
    return open_connection()

def read_items(session=Depends(get_db_session)):
    return query(session)

Depends(fn) marks a parameter to be resolved by calling fn first — the endpoint receives a plain value, never touches Depends or fn directly, which is what makes app.dependency_overrides able to swap it for tests.

6.Testing with injected dependencies

Because OrderService receives its PaymentGateway as a parameter, a test can pass a FakePaymentGateway that records calls instead of making a real charge. No network call happens, and the test can assert exactly what was charged.

Key features

  • A fake implements the same methods as the real dependency, so it satisfies whatever the class calls
  • A fake can record what was called with, which a test then asserts against
  • Passing a fake through the constructor needs no change to the class under test — the injection point already exists
  • A test that can't inject a fake usually means the dependency was constructed inside the class, not received as a parameter
  • This is why constructor and function injection matter in practice, not just in theory
Same OrderService, two different PaymentGateways

Production

  • OrderService(PaymentGateway())
  • charge() makes a real network call
  • Used when the app actually runs

Test

  • OrderService(FakePaymentGateway())
  • charge() records the amount, returns a canned result
  • Used only inside the test — no network call
  • Production
    • OrderService(PaymentGateway())
    • charge() makes a real network call
    • Used when the app actually runs
  • Test
    • OrderService(FakePaymentGateway())
    • charge() records the amount, returns a canned result
    • Used only inside the test — no network call

Command

python
class FakePaymentGateway:
    def __init__(self):
        self.charged = []

    def charge(self, amount):
        self.charged.append(amount)
        return {"status": "ok", "amount": amount}

service = OrderService(FakePaymentGateway())

A class that receives its dependency as a parameter can be tested with a fake that records calls instead of doing the real, slow, or dangerous thing.

7.Avoiding hidden global dependencies

A hidden global dependency is module-level state a function reads or changes without it appearing in the function signature. fetch_user(user_id) that secretly mutates a global connection pool gives different results for the same input, and cannot be tested in isolation.

Key features

  • A function whose output depends on a module-level global has an undeclared dependency — its signature lies about what it needs
  • Two calls with identical arguments can return different results if hidden global state changed in between
  • A hidden global cannot be swapped for a test fake — there is no parameter to inject one through
  • Passing the same value explicitly (pool.fetch_user(user_id)) makes the dependency visible and swappable
  • This is the concrete problem constructor and function injection both solve

Command

python
# Hidden dependency — reads/mutates a module global
def fetch_user(user_id):
    _connection_pool["queries"] += 1
    return _connection_pool["queries"]

# Explicit dependency — visible in the signature
def fetch_user(pool, user_id):
    return pool.fetch_user(user_id)

If a function's result depends on something not in its parameter list, that dependency is hidden — make it a parameter so it is visible, swappable, and testable.

Remember

  • Depend on the abstraction (what a dependency must do), not the concrete class — that is what makes swapping it possible.
  • Pass a dependency into __init__ or a function parameter; do not build it with ClassName() inside the code that uses it.
  • A container maps names to factories and resolves a dependency chain automatically — worth it once wiring by hand gets repetitive.
  • If a function's result depends on something not in its parameter list, that dependency is hidden — make it a parameter.

Pro tip

  • FastAPI's Depends(fn) is function injection: the endpoint receives fn's return value as a plain argument, resolved before the endpoint runs.
  • A fake that records what it was called with turns an assertion about behavior into an assertion about a stored list.
  • A container commonly caches an instance after first building it, so resolve() returns the same object on every later call.
  • A test that cannot substitute a dependency is usually a sign the dependency was constructed inside the class, not injected.

Next up

  • Linux

Sources

  • abc — docs.python.org/3/library/abc.html
  • FastAPI dependencies — fastapi.tiangolo.com/tutorial/dependencies
  • Every example executed on Python 3.14.3 · checked 2026-08-22

Spotted a mistake on this page? Tell us.