CodeRigor

FREE LESSON

Interfaces, polymorphism & justified inheritance

Program to a small pricing protocol and use inheritance only when a subclass preserves the same callable contract.

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.

One consumer, several behaviors
start / 2
Checkout calls the charge interface. UnitRate, its CappedRate subclass and an unrelated policy can all satisfy that interface.
One consumer, several behaviors
Checkout calls the charge interface. UnitRate, its CappedRate subclass and an unrelated policy can all satisfy that interface.
Python
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.

A cap applies to one charge
start / 1
With unit price thirty and cap seventy, a capped rate never returns more than seventy for a single call.
A cap applies to one charge
With unit price thirty and cap seventy, a capped rate never returns more than seventy for a single call.

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.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Watch subclass initialization, overriding and super(). Compare CappedRate against UnitRate’s full contract. The lesson separately introduces structural Protocol typing; the video is not a Protocol reference.