We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Type System as a Design Tool step 19 of 24
@override and the notifier that stopped notifying
Rename Base.handle to Base.process. Forget one subclass. Now Child.handle
is dead code that no test exercises, because every call goes through the base
class’s implementation instead — and the base class’s implementation is
plausible, so nothing crashes. It just quietly stops doing the thing the
subclass was written to do.
This is one of the highest-frequency, lowest-visibility refactoring bugs in
object-oriented Python, and @override (PEP 698, Python 3.12) costs one line to
eliminate:
class EmailNotifier(Notifier):
@override
def deliver(self, message: str) -> bool: ...
The checker now verifies that a method of that name exists on some base. Rename
the base method and Child.deliver becomes an error at the definition site,
naming the method that no longer overrides anything.
Two things to know:
-
explicit-overrideis not in--strict. Plain@overridecatches the method that claims to override and does not. The reverse direction — a method that overrides without saying so — needs--enable-error-code explicit-override, and that is what this problem is graded with. Turning it on in an existing codebase produces a large, boring, entirely mechanical diff, and afterwards every override in the repo is machine-checked. -
Decorator ordering matters.
@overridegoes innermost, closest to thedef, below@property,@staticmethod,@functools.cacheand friends. Above them it decorates the wrong object.
The task
class Notifier(ABC):
@abstractmethod
def deliver(self, message: str) -> bool: ...
class EmailNotifier(Notifier): ... # delivers when 0 < len(message) <= 100
class SmsNotifier(Notifier): ... # delivers when 0 < len(message) <= 20
def dispatch(notifiers: Sequence[Notifier], message: str) -> list[bool]: ...
def solve(kinds: list[str], message: str) -> tuple[list[bool], int, list[str]]: ...
Both subclasses carry @override. dispatch returns one result per notifier,
in order.
solve maps the strings "email" and "sms" to instances, in order; any other
string is skipped and recorded. It returns
(dispatch results, number of successful deliveries, skipped kinds).
The length bounds are inclusive on the upper side and exclusive of the empty string: a 20-character SMS delivers, a 21-character one does not, and an empty message delivers nowhere.
ABC or Protocol?
Notifier is an ABC here, deliberately, because the two implementations are
ours and inheritance is documenting a deliberate hierarchy. Use an ABC when
you own every implementation and want a shared base, abstractmethod‘s
instantiation guard, and shared helper methods. Use a Protocol when the
implementations are somebody else’s and demanding they subclass you would be
viral. That is the T2 story; the relevant part here is that @override applies
to both.
Worth knowing while you are here: an explicit Protocol subclass that omits a
member is a mypy [abstract] error but instantiates fine at runtime — the
runtime guard you get from ABC is not something Protocol gives you.
Types
dispatch takes Sequence[Notifier] rather than list[Notifier] — it only
iterates and indexes, so accepting a tuple costs nothing. See the variance
problems for why that matters more than it looks.
sum(results) on a list[bool] is an int, because bool is an int
subclass. That is normally the trap; here it is the feature.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.