runtime
145 / 250
lesson 145 Advanced OOP & Design · advanced

Dependency injection: pass collaborators in

An object that creates its own clock, price list or storage can only ever use those. Passing them in through the constructor lets the same class run against real services, test fakes or a different supplier, without changing its code.

You'll be able to
  • Recognise hard-wired dependencies (datetime.now(), a fixed file name, a service created inside __init__)
  • Receive collaborators as constructor parameters, with sensible defaults where useful
  • Test the class with small fakes that make results predictable
The idea

A class that needs the current date can call date.today() itself, and then a test can't check what happens at month end without waiting for month end. A class that opens prices.csv in its constructor can't run without that exact file. These are hard-wired dependencies.

Dependency injection means the object *receives* its collaborators: InvoiceService(clock), Checkout(prices, orders, notifier). The class only relies on what it needs from them (a today() method, a save method: a Protocol, the Data Models & Typing module). In production you pass the real ones; in tests you pass small fakes whose answers you choose. You have already done this with functions (passing get_rate and send_email in the Testing & Code Quality module); here it is done for whole objects.

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · Due dates with an injected clock
from datetime import date, timedelta

class FixedClock:
    def __init__(self, day):
        self.day = day

    def today(self):
        return self.day

class InvoiceService:
    def __init__(self, clock, payment_days=30):
        self.clock = clock
        self.payment_days = payment_days

    def due_date(self):
        return self.clock.today() + timedelta(days=self.payment_days)

    def is_overdue(self, due):
        return self.clock.today() > due

service = InvoiceService(FixedClock(date(2026, 10, 20)))
print(service.due_date())
print(service.is_overdue(date(2026, 10, 19)), service.is_overdue(date(2026, 10, 20)))
later = InvoiceService(FixedClock(date(2026, 12, 31)), payment_days=14)
print(later.due_date())
example-2.py · A hard-wired price file
import csv

class PriceList:
    def __init__(self):
        with open("prices.csv", encoding="utf-8") as f:
            self.prices = {row["sku"]: float(row["price"]) for row in csv.DictReader(f)}

    def price(self, sku):
        return self.prices[sku]

class Checkout:
    def __init__(self):
        self.prices = PriceList()

    def total(self, skus):
        return round(sum(self.prices.price(sku) for sku in skus), 2)

print("creating checkout")
checkout = Checkout()
example-3.py · A checkout with everything passed in
class InMemoryOrders:
    def __init__(self):
        self.orders = {}

    def save(self, order_id, total):
        self.orders[order_id] = total

class ListNotifier:
    def __init__(self):
        self.sent = []

    def send(self, message):
        self.sent.append(message)

class Checkout:
    def __init__(self, prices, orders, notifier):
        self.prices = prices
        self.orders = orders
        self.notifier = notifier

    def place(self, order_id, skus):
        total = round(sum(self.prices[sku] for sku in skus), 2)
        self.orders.save(order_id, total)
        self.notifier.send(f"{order_id} placed: {total}")
        return total

orders = InMemoryOrders()
notifier = ListNotifier()
checkout = Checkout({"SPO-11681": 31.0, "HOM-22847": 48.0}, orders, notifier)
print(checkout.place("O100282", ["SPO-11681", "HOM-22847"]))
print(orders.orders, notifier.sent)

When to use it: for anything slow, external, random or time-dependent: clocks, network services, databases, files, random numbers, email. Give parameters sensible defaults where there is an obvious real implementation, so everyday callers needn't pass them.

Mistakes to avoid: creating collaborators inside __init__ (hard-wired again); reaching into global state such as module-level connections; injecting so many collaborators that the class is doing too much (split it); and fakes that behave differently from the real thing in ways the tests rely on.

Review
  • Hard-wired dependencies (the real clock, a fixed file, a service built inside __init__) make classes hard to test and change.
  • Pass collaborators in through the constructor; the class relies only on the methods it calls.
  • Tests pass small fakes (a fixed clock, an in-memory store) and get predictable results.
  • Next module: packaging projects and storing data in SQLite.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Dependency injection: pass collaborators in — Advanced OOP & Design · Runtime