Agree on a small promise
Picture
A reminder needs to send one message. It should not need to know network credentials just to decide the wording. A sender with a small send(message) promise can be passed in from outside.
Predict
How can a test verify the reminder message without contacting an email service?
Walk through the small example
Supply a recording sender that appends messages to a list. The reminder keeps the same workflow and can be tested without external side effects.
Mental model and complete Python solution
The Protocol describes the structural interface expected by a type checker. Reminder accepts a collaborator through its constructor: dependency injection. RecordingSender satisfies the shape without inheriting from Protocol. The contract says send returns normally when accepted or raises on failure; Reminder does not swallow that failure. Type annotations are not runtime validation and cannot prove delivery semantics. A real email adapter would implement this boundary separately.
What the function promises: Reminder.send_for(name) builds and submits one message to its supplied sender. RecordingSender only records in memory and never performs real delivery.
from typing import Protocol
class Sender(Protocol):
def send(self, message: str) -> None: ...
class RecordingSender:
def __init__(self):
self.messages = []
def send(self, message: str) -> None:
self.messages.append(message)
class Reminder:
def __init__(self, sender: Sender):
self.sender = sender
def send_for(self, name: str) -> None:
self.sender.send(f"{name}, your saved idea is ready to revisit.")
sender = RecordingSender()
Reminder(sender).send_for("Ana")
assert sender.messages == ["Ana, your saved idea is ready to revisit."]
assert RecordingSender().messages == []
Work and storage: Message creation is linear in message length; delivery cost belongs to the sender implementation. The value of injection is controllability and separation, not an asymptotic improvement.
When to recognize it: The service needs a send operation, not knowledge of how delivery happens.
Check before applying it: Python type hints do not validate behavior at runtime. The replacement must honor the same error and side-effect contract.
Change one rule
A replacement sender silently drops all messages and returns normally. Does matching the method signature make it a valid replacement?
See what changes
No. It violates the acceptance/delivery promise agreed for the boundary. Substitution is about observable behavior as well as method names and types.
Connect
functions: Input and output contracts still matter between objects.
Make failure and retries part of the design: Inject a controlled collaborator to test failure and retry behavior.