Notes and exercises for learning design patterns
AttributeError: 'LoggingDecorator' object has no attribute 'set_from_address'
When Python evaluates notifier.set_from_address(...), it looks for set_from_address on the LoggingDecorator instance. It does not find it — because LoggingDecorator only declares send. Python raises AttributeError before it ever thinks to check the wrapped EmailNotifier.
The decorator has hidden the inner object’s richer interface. This is type erosion.
class NotifierDecorator(Notifier):
def send(self, recipient: str, message: str) -> None:
self._wrapped.send(recipient, message)
def set_from_address(self, address: str) -> None:
self._wrapped.set_from_address(address)
def set_reply_to(self, address: str) -> None:
self._wrapped.set_reply_to(address)
def get_sent_count(self) -> int:
return self._wrapped.get_sent_count()
def flush_queue(self) -> None:
self._wrapped.flush_queue()
Problems:
self._wrapped. They add zero logic.EmailNotifier gets set_bcc next week, you must update NotifierDecorator and every decorator that overrides it.This is the scaling problem __getattr__ solves in one line.
class NotifierDecorator(Notifier):
def __init__(self, wrapped: Notifier):
self._wrapped = wrapped
def send(self, recipient: str, message: str) -> None:
self._wrapped.send(recipient, message)
def __getattr__(self, name: str):
return getattr(self._wrapped, name)
That is the entire change. All four forwarding methods are gone.
__getattr__ fires only when Python’s normal attribute lookup fails — which means it only fires for attributes the decorator does not declare itself. The decorator’s own send method is found by normal lookup and never reaches __getattr__.
The lookup for notifier.set_from_address:
1. LoggingDecorator instance dict → not found
2. LoggingDecorator and NotifierDecorator → not found
3. NotifierDecorator.__getattr__("set_from_address")
→ getattr(self._wrapped, "set_from_address")
→ EmailNotifier.set_from_address
notifier = LoggingDecorator(
RateLimitDecorator(
PrefixDecorator(EmailNotifier(), prefix="[ALERT] "),
limit=10,
)
)
notifier.set_from_address("alerts@company.com")
The chain:
LoggingDecorator.__getattr__("set_from_address")
→ getattr(RateLimitDecorator, "set_from_address")
→ RateLimitDecorator.__getattr__("set_from_address")
→ getattr(PrefixDecorator, "set_from_address")
→ PrefixDecorator.__getattr__("set_from_address")
→ getattr(EmailNotifier, "set_from_address")
→ EmailNotifier.set_from_address ✓
Each layer delegates to the next until the method is found. This works for any depth of nesting, with no changes to any individual decorator.
For notifier.get_sent_count() — the count comes from EmailNotifier._sent_count, which is incremented by EmailNotifier.send. PrefixDecorator transforms the message but calls EmailNotifier.send underneath, so the count is correct.
def configure_notifier(notifier: EmailNotifier) -> None:
notifier.set_from_address("alerts@company.com")
configure_notifier(LoggingDecorator(EmailNotifier())) # runtime: fine
# mypy: error
At runtime: works. __getattr__ forwards set_from_address to the inner EmailNotifier.
To a type checker: LoggingDecorator is a Notifier, not an EmailNotifier. The type checker sees that LoggingDecorator only declares send, and correctly reports that passing it where EmailNotifier is expected is a type error.
The type checker is not wrong. It is pointing out a real design tension: the function signature says “I need the full EmailNotifier interface” but you are passing something that only promises Notifier. The fact that it works at runtime due to __getattr__ is invisible to static analysis.
The trade-offs:
| Approach | Runtime | Type checker |
|---|---|---|
__getattr__ forwarding |
Works | Sees Notifier, not EmailNotifier |
| Manual forwarding methods | Works | Still sees Notifier unless you widen the base class |
# type: ignore at call site |
Works | Suppressed |
Accept Notifier in the function signature |
Works | Fine, but loses EmailNotifier-specific guarantees |
The transparent decorator trades static type visibility for zero maintenance cost on the forwarding layer. For most decorator use cases this is the right trade-off — the shared interface covers what matters, and the forwarded methods are incidental configuration. If static safety for those methods matters, you can widen the abstract base, use a Protocol, or add __getattr__ stubs.
| Exercise | Core skill |
|---|---|
| 1 | Implement the decorator structure: same interface, wraps in constructor, calls through |
| 2 | Compose multiple decorators and reason about ordering |
| 3 | Fix type erosion with __getattr__ so the decorator is transparent to the full interface |
The progression mirrors a real development workflow: you build it, you add more of them, and then the wrapped object’s interface grows and you hit the type erasion problem and solve it.