Application Design and Contracts
Protocols and Injected Dependencies
Separate policy from infrastructure with structural interfaces that remain simple to fake in tests.
Lesson 2 of 6 in the recommended order · About 25 min (estimate)
On this page
Outcome
Define a Protocol and inject an implementation without coupling domain logic to a concrete client.
Why it matters
At this level, code is judged by how safely it changes under load, failure, and team ownership. The technique in this lesson makes an important boundary visible enough to test and review.
Concept
A Protocol describes the behavior a collaborator must provide. Business logic can accept that behavior through a constructor or function argument, while production and tests supply different implementations. This is structural guidance for a static type checker; Python does not automatically validate the full protocol when the argument is passed at runtime.
Read the code
from typing import Protocol
class Clock(Protocol):
def today(self) -> str: ...
class FixedClock:
def today(self) -> str:
return "2026-09-05"
def report_name(clock: Clock) -> str:
return f"report-{clock.today()}.csv"
print(report_name(FixedClock()))
Read from the public behavior inward: identify the input boundary, the decision, and the observable result before studying syntax.
One required behavior, one supplied clock
1. Static shape: Clock
Required method- today() -> str
Function boundary- report_name(clock: Clock)
A static type checker can compare the supplied object with this shape. FixedClock needs no Clock base class.
2. Runtime collaborator: FixedClock
Caller supplies FixedClock()
report_name calls clock.today()
Python invokes the method on the actual supplied object. The Clock annotation alone does not validate the full protocol at call time.
3. Observable boundary
report_name returns filename text
print displays that text
The worked code does not read system time or write a file. Predict the exact printed string before opening the answer.
Predict the output
Predict the exact output before running the example.
Check your prediction
It prints report-2026-09-05.csv. report_name depends on behavior, not on system time or a particular class.
Modify the code
Change one valid input into the nearest invalid or overloaded case. Write down which layer should reject it and what the caller should observe.
Review the change
Keep the failure at the narrowest boundary that owns the rule. Preserve a stable return value or exception contract so callers do not need to inspect implementation details.
Debug the bug
Creating the real client inside report_name hides the dependency and makes failure cases hard to test. Passing it in makes ownership and lifetime explicit.
Try it yourself
Complete the focused implementation and run its deterministic checks.
Loading this exercise…
Practical challenge (optional)
Define a Store protocol with save(name, text), then implement a memory store and test a function without touching disk.
Sign in to track your progress on this exercise.
AI collaboration
Checkpoint
- Why does
FixedClocksatisfyClockwithout inheriting from it? - Does the annotation
clock: Clockperform full runtime validation when the function is called? - What testability problem appears if
report_nameconstructs the real clock or client itself?
Answers
- Protocols use structural typing: a static type checker sees that
FixedClockprovides the requiredtodaymethod. - No. Ordinary annotations guide tools; they do not automatically check the complete protocol at runtime.
- The dependency and its lifetime become hidden, so a deterministic fake and failure cases are difficult to supply.
Sign in to track your progress on this exercise.
Summary and next step
You made the boundary explicit, predicted its behavior, tested a deterministic implementation, and examined its failure mode. Continue to the next lesson to combine this technique with a wider application or production constraint.