Checkout needs a collaborator that can answer one question: what does this
number of units cost? The useful interface is charge(units) -> integer cents.
A caller using that interface can select uniform pricing, capped pricing or a
special policy without rewriting Checkout for every concrete class.
Polymorphism means the same operation dispatches to behavior supplied by different
objects. In Python, that often works structurally: compatible classes provide
the required method without sharing a base class. typing.Protocol lets a type
checker describe this expectation. It does not automatically validate return
values, enforce invariants or prevent a runtime call with an incompatible object.
from typing import Protocol
class Price(Protocol):
def charge(self, units: int) -> int:
...
class Uniform:
def charge(self, units):
return units * 20
class Capped(Uniform):
def charge(self, units):
return min(super().charge(units), 50)
class Promotional:
def charge(self, units):
return 0 if units == 0 else 10
def total(policy: Price, quantities):
return sum(policy.charge(n) for n in quantities)
for policy in (Uniform(), Capped(), Promotional()):
print(total(policy, [0, 2, 4]))The example assumes valid nonnegative integer counts. The build requires the
concrete rates to check their input contract. Promotional is unrelated by
inheritance, but it still supplies the needed behavior. Checkout should call
charge, not inspect private fields or branch on type(policy).
Inherit only when substitution makes sense
CappedRate inherits UnitRate to reuse unit-price setup and validated base
calculation. Its override calls super().charge(units) and applies an upper
bound. This is justified only because the shared interface promises nonnegative
integer pricing for nonnegative counts; it does not promise that every policy
uses an uncapped linear formula.
A subclass must retain accepted inputs and relevant error behavior. For example, a capped rate that rejects zero while the base accepts it would surprise code written against that contract. Reusing lines of code alone is insufficient reason for inheritance. A separate policy with the same method is often simpler when behavior or state no longer has the base class’s meaning.
Protocol differs from an abstract base class. An ABC can require concrete
subclasses to implement abstract methods before instantiation. A protocol
primarily supports structural static checking. Even @runtime_checkable checks
attribute presence rather than proving full signatures or semantic correctness.
This lab needs no runtime protocol test: its behavioral tests are the evidence.
The cap is applied to each call, so two capped charges may exceed one cap when summed. State the unit of the policy before writing a loop. Checkout calls once per input count in order, permits an empty list, and propagates a collaborator error. A recording fake checks that behavior without any external service.
Use the checks to distinguish structural typing from runtime validation and composition from inheritance. Repair the invalid-count and wrong-cap bugs, fill the dispatch loop, then implement both rates and Checkout. Test a new unrelated policy to prove your consumer depends on behavior rather than concrete ancestry.