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.
- 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
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.
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())
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()
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.
- 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.