Inheritance and Polymorphism
Design class hierarchies that survive change: override methods with super(), read the C3 method resolution order, enforce contracts with abstract base classes, and choose composition or protocols when inheritance is the wrong tool
A checkout service added a FreeShippingCoupon class that inherited from two
parents: Coupon, which held the discount rules, and ShippingRule, which held
the minimum order value. Both parents defined __init__, and the new class
called only the first one. The coupon applied correctly, but every order under
20 dollars shipped free, because the shipping rule never received its
min_order attribute.
The bug survived review because the class looked like a natural description of the business: a free shipping coupon is a coupon, and it is a shipping rule. Inheritance is not a description of reality. It is a promise about code, and the promise is that the subclass can stand in for the parent everywhere.
This lesson shows how that promise is enforced, how Python resolves a method when several parents define it, and when the honest answer is to stop inheriting and compose instead. It builds directly on the class model from lesson 1: attributes, instance state and bound methods are the raw material, and inheritance is the layer that organises them.
Key terms
- A class that another class inherits from, also called a parent or superclass.
- A class that inherits attributes and methods from one or more base classes.
- A method in a subclass that replaces the implementation found earlier in the MRO.
- The method resolution order, a linear list of classes that Python searches from most specific to object.
- The algorithm that builds the MRO while preserving local precedence and monotonicity.
- A design where every class in a multiple inheritance chain calls super() so each parent runs once.
- A class that declares methods a subclass must implement and cannot be instantiated until it does.
- A typing construct that describes required attributes and methods, so any matching class qualifies without inheriting.
- A small class that adds one behaviour to a hierarchy and is not meant to be instantiated on its own.
- A design where an object holds other objects and delegates work to them instead of inheriting their behaviour.
- The rule that a subclass must be usable anywhere its base class is expected without breaking callers.
Base class
Subclass
Override
MRO
C3 linearization
Cooperative inheritance
Abstract base class
Protocol
Mixin
Composition
Liskov substitution
1. What Inheritance Actually Promises
A subclass inherits two things: the interface, meaning the names a caller may use, and the implementation behind those names. The interface is the useful part. It lets a function accept a base class and work with every subclass that honours the same contract.
The exporter below is the running example for this lesson. The base class holds the destination and the shared method name. Two subclasses provide the actual format.
import csv
import io
import json
class Exporter:
def __init__(self, destination: str) -> None:
self.destination = destination
def format_name(self) -> str:
return "unknown"
def render(self, rows: list[dict]) -> str:
raise NotImplementedError
class CsvExporter(Exporter):
def format_name(self) -> str:
return "csv"
def render(self, rows: list[dict]) -> str:
buffer = io.StringIO()
writer = csv.DictWriter(buffer, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
return buffer.getvalue()
class JsonExporter(Exporter):
def format_name(self) -> str:
return "json"
def render(self, rows: list[dict]) -> str:
return json.dumps(rows, indent=2)The caller below never mentions either subclass. It accepts an Exporter and
calls the two methods the base class defines. That is polymorphism in its
plainest form: one function, several concrete types, chosen at call time.
rows = [
{"order": 1001, "total": 4999},
{"order": 1002, "total": 12900},
]
for exporter in (CsvExporter("reports"), JsonExporter("reports")):
output = exporter.render(rows)
print(exporter.format_name(), len(output.splitlines()), len(output))The output is csv 3 36, then json 10 95. The caller did not branch on the
type to produce either result.
The structure fits in one diagram. It separates the shared base contract from the two concrete renderers.
Description
A class diagram of Exporter with destination, format_name and render, and two subclasses CsvExporter and JsonExporter that override format_name and render.
The open arrow means "inherits from". Every exporter shares the destination attribute and the two method names, and the subclasses supply the format.
Inheritance is the tightest coupling in object-oriented Python. A change to a base class reaches every subclass, including subclasses in other packages that you cannot see. Add a base class only when the shared contract is stable and the is-a claim is true.
The is-a test is stricter than it sounds. A Square is a rectangle in
geometry, but a mutable square is not a well-behaved mutable rectangle, as
section 8 shows. A FreeShippingCoupon is a coupon, but the shipping rule was
a second responsibility, not a second identity. The next sections show the
mechanics, and the mechanics explain why these distinctions matter.
2. Overriding and the Real Meaning of super()
A subclass overrides a method by defining it again. The new definition wins
for instances of the subclass, and the base implementation remains available
through super(). That built-in returns a proxy bound to the current instance
and the current class, and attribute access on the proxy continues the search
after the current class in the MRO.
The exporter below adds a header without copying the CSV logic. It calls the parent implementation and decorates the result.
class HeaderedExporter(CsvExporter):
def render(self, rows: list[dict]) -> str:
output = super().render(rows)
return f"# generated by {type(self).__name__}\n" + output
print(HeaderedExporter("reports").render(rows).splitlines()[0])The output is # generated by HeaderedExporter. The CSV rows are still
produced by CsvExporter.render, and the subclass only wraps them.
The sequence below follows that call. The key step is the third one, where the proxy resumes the search at the next class instead of restarting at the base.
Description
A sequence diagram of HeaderedExporter.render calling super, which reaches CsvExporter.render, which builds the CSV text and returns it, after which the subclass prepends a header line.
Two details trip people up. First, super() does not mean "the parent". It
means "the next class in this instance's MRO", which matters under multiple
inheritance. Second, super() works inside class methods and static methods
too, with different binding rules, and the zero argument form relies on a
compiler supplied __class__ cell.
Pass extra constructor arguments as keywords through **kwargs so every
class in a cooperative chain receives the arguments it owns. The pattern is
shown in section 3 and it is the only reliable way to initialise a diamond.
The video below walks through the same override mechanics with a different example, so you can watch a subclass replace and extend a parent method.
Description
A video lesson that creates a subclass, overrides a method, extends the parent with super, and shows which implementation runs for each instance.
A common mistake is forgetting the parent constructor. A subclass that stores
its own attribute but never calls super().__init__ leaves the inherited state
unset, and the failure surfaces later as an AttributeError on the first read
of the missing attribute.
The matching exercise below connects each mechanism to the effect a caller observes.
How to answer
Matching exercise with 5 pairs. Match each item on the left with its answer on the right.
Match each inheritance mechanism to the effect it has at call time.
Drag answers from here:
Since Python 3.12, the typing module provides @override for exactly this
last case. It tells a type checker that the method is meant to replace an
ancestor method, so a rename in the base class becomes an error instead of a
silent fork.
from typing import override
class AuditExporter(JsonExporter):
@override
def render(self, rows: list[dict]) -> str:
return super().render(rows)The decorator has no runtime enforcement. It sets __override__ on the
function when possible and leaves the real check to tools such as mypy, pyright
or pyre. That is still valuable: a base class rename now fails in continuous
integration instead of shipping a method that nothing calls.
3. The MRO and the Diamond Problem
When several base classes define the same name, Python needs a single order. That order is the method resolution order, computed by the C3 linearization algorithm. The algorithm merges the MROs of the parents and the list of parents themselves, preserving local precedence and monotonicity.
The diamond below is the smallest interesting case. B and C both inherit
from A, and D inherits from both.
class A:
def label(self) -> str:
return "A"
class B(A):
def label(self) -> str:
return "B"
class C(A):
def label(self) -> str:
return "C"
class D(B, C):
pass
print(D().label())
print([cls.__name__ for cls in D.__mro__])The output is B and then ['D', 'B', 'C', 'A', 'object']. Python tries D,
finds no label, tries B, finds one, and stops. The full list is available
on every class through __mro__ and through the mro() method.
The diagram below draws the search order rather than the inheritance graph, so the difference between "who inherits from whom" and "who is checked first" is visible.
Description
A top down flowchart of attribute lookup for class D, checking D, then B where label is found, then C, then A, then object, with the found branch coloured green.
C3 is not depth-first search. A naive depth-first walk would visit D, B,
A, then C, which would let A win over C. C3 instead guarantees that a
class appears before its parents and that the order of each parent list is
preserved. When no order can satisfy both rules, Python refuses to build the
class.
The failure is easy to reproduce. Declaring class Left(X, Y) and
class Right(Y, X), then class Conflict(Left, Right), raises TypeError: Cannot create a consistent method resolution order (MRO) for bases X, Y. The
two parents disagree about whether X or Y comes first, and no linear order
can satisfy both. This is a design error, not a runtime condition to catch, and
the fix is to change the hierarchy rather than the algorithm.
Cooperative inheritance is the pattern that makes diamonds work. Every class
accepts **kwargs, takes the arguments it owns, and forwards the rest.
class Root:
def __init__(self, **kwargs) -> None:
print("Root", kwargs)
super().__init__()
class Left(Root):
def __init__(self, left: int, **kwargs) -> None:
print("Left", left, kwargs)
self.left = left
super().__init__(**kwargs)
class Right(Root):
def __init__(self, right: int, **kwargs) -> None:
print("Right", right, kwargs)
self.right = right
super().__init__(**kwargs)
class Leaf(Left, Right):
def __init__(self, left: int, right: int) -> None:
print("Leaf", left, right)
super().__init__(left=left, right=right)
leaf = Leaf(left=1, right=2)
print(leaf.left, leaf.right)The output is a trace of the chain: Leaf 1 2, Left 1 {'right': 2},
Right 2 {}, Root {}, then 1 2. Each constructor ran once, and each
received only the keyword it owned. The MRO for Leaf is Leaf, Left,
Right, Root, object.
Never call a specific parent constructor by name in a cooperative hierarchy.
Writing Root.__init__(self) inside Left runs the root twice in a diamond
and skips the classes in between. super() is the only call that respects
the MRO.
The fill in the blank exercise checks the vocabulary that the trace depends on.
How to answer
Fill-in-the-blank exercise with 4 blanks. Type the missing words, then check your answer.
Complete the description of method resolution.
Python searches for an attribute along the blank, which is built by the blank algorithm. A subclass reaches the next class in that order with blank. When two parents disagree about the order of their own bases, class creation raises blank.
Word Bank, drag words into the blanks above:
4. Abstract Base Classes: Contracts With Teeth
The base Exporter in section 1 raised NotImplementedError only when a
subclass forgot to implement render. Nothing stopped a caller from creating
the base class itself, and nothing stopped a subclass from inheriting the
placeholder. An abstract base class turns both mistakes into an immediate
error.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
def describe(self) -> str:
return f"{type(self).__name__} area {self.area():.2f}"
class Rect(Shape):
def __init__(self, width: float, height: float) -> None:
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.heightThe class cannot be instantiated while an abstract method is unimplemented. The console session below shows the exact failure and the working subclass.
try:
Shape()
except TypeError as exc:
print(type(exc).__name__, exc)
print(Rect(5, 3).describe())
print(Shape.__abstractmethods__)The output is TypeError Can't instantiate abstract class Shape without an implementation for abstract method 'area', then Rect area 15.00, then
frozenset({'area'}). The check happens at instantiation, not at class
creation, so a subclass can be defined first and completed later.
Abstract methods may carry an implementation. A subclass can call it through
super(), which is how frameworks provide a default that subclasses extend
rather than replace. The describe method above is the smaller
version of that idea: it is concrete, and it calls the abstract area that
each subclass must supply. That pattern is called the template method, and it
is the main reason to put logic in an abstract base rather than an interface
alone.
Abstract properties and class methods need abstractmethod as the innermost
decorator, closest to the function, so that property or classmethod wraps
the abstract mark instead of replacing it. Reversing the two produces a method
that looks abstract to a reader and is not abstract to Python.
An ABC can also declare that an unrelated class satisfies it without
inheriting. ABCMeta.register adds a virtual subclass, and __subclasshook__
can make the check structural.
class Exportable(ABC):
@abstractmethod
def render(self, rows: list[dict]) -> str:
...
@classmethod
def __subclasshook__(cls, candidate):
if cls is Exportable and any(
"render" in base.__dict__ for base in candidate.__mro__
):
return True
return NotImplemented
class LegacyReporter:
def render(self, rows):
return "legacy"
print(issubclass(LegacyReporter, Exportable))
print(isinstance(LegacyReporter(), Exportable))Both lines print True. The hook checked the class dictionary of every class
in the candidate MRO and found render, so the legacy class counts as an
Exportable without inheriting anything. The catch is that registration and
hooks change the answer to isinstance; they do not give the class the
methods or the abstract enforcement. LegacyReporter still cannot be used
where the code expects an actual Exportable subclass with inherited
behaviour.
5. Duck Typing and Protocols
Python does not require a shared base class to call a shared method. The
publish function below accepts anything with a render method, which is duck
typing: the object's behaviour decides, not its ancestry.
def publish(renderer, rows: list[dict]) -> None:
print(renderer.render(rows).splitlines()[1])
publish(CsvExporter("reports"), rows)The output is 1001,4999. The function has no annotation on renderer, so
nothing is checked before the call. If the object lacks render, the failure
appears at the call site as AttributeError, which is late but clear.
Static type checkers cannot verify an unannotated duck-typed parameter. A
Protocol fixes that by describing the required shape once, without forcing
anyone to inherit.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Renderer(Protocol):
def render(self, rows: list[dict]) -> str: ...
def publish_typed(renderer: Renderer, rows: list[dict]) -> None:
print(renderer.render(rows).splitlines()[1])
publish_typed(CsvExporter("reports"), rows)
print(isinstance(CsvExporter("reports"), Renderer))The output is 1001,4999 and True. CsvExporter never mentions Renderer,
yet it satisfies the protocol because it has a compatible render method.
Static checkers accept the call, and the runtime check works because the
protocol is marked runtime_checkable.
The table below separates the three tools, which are often confused.
Tool | Binds by | Runtime check | Static check | Best for |
|---|---|---|---|---|
Duck typing | Method presence at call time | No | No | Small internal helpers |
Protocol | Method shape | Only with runtime_checkable | Yes | Third-party or unrelated classes |
ABC | Explicit inheritance or registration | Yes | Yes | Contracts you own and control |
runtime_checkable checks names, not signatures. A class with render(self)
passes the isinstance check even if it takes no rows argument, and the
failure moves to the call. Treat the runtime check as a cheap filter and let
the type checker enforce the signature.
Protocols also cover inherited behaviour, because the lookup walks the MRO. A
subclass of a class that defines render satisfies the protocol with no
registration step.
The decision is usually about ownership. Use an ABC when you own the hierarchy and want to share implementation. Use a Protocol when the classes come from elsewhere, or when you want the interface documented without forcing a base class on every implementer. Use plain duck typing when the call is local and a type checker is not watching.
6. Mixins and Cooperative Multiple Inheritance
A mixin is a class that adds one behaviour to a hierarchy without pretending
to be a complete object. It usually has no constructor, no state of its own,
and a method that calls super(). The pattern works because the MRO places the
mixin before the concrete class and the super() chain reaches the real
implementation.
class LoggingMixin:
def render(self, rows):
output = super().render(rows)
print(f"logged {len(output)} chars")
return output
class RetryMixin:
def render(self, rows):
for attempt in range(1, 3):
try:
return super().render(rows)
except RuntimeError:
print("retry", attempt)
raise
class JsonWithMixins(LoggingMixin, RetryMixin, JsonExporter):
pass
print(JsonWithMixins("reports").render(rows)[:1])
print([cls.__name__ for cls in JsonWithMixins.__mro__])The output is logged 95 chars, then [, then the MRO:
['JsonWithMixins', 'LoggingMixin', 'RetryMixin', 'JsonExporter', 'Exporter', 'object']. The logging mixin ran first because it is listed first, and its
super() call reached the retry mixin, which reached the JSON exporter.
Swap the first two bases and the order of the observable effects changes while the final output stays the same. That is the point and the danger: mixin order is part of the program, and a reader cannot infer it from the method names.
Description
A class diagram of JsonWithMixins inheriting from LoggingMixin, RetryMixin and JsonExporter, with arrows showing the linear chain that super follows through each mixin to the exporter.
The diagram shows the linear chain the MRO produces, not the source order of the class statement. Django's class-based views are built from exactly this pattern, and the documentation warns that not every mixin combines with every view.
A mixin that stores state or defines __init__ stops being a mixin and
becomes a base class with hidden ordering requirements. Keep mixins small,
stateless and cooperative, or move the behaviour into a composed object.
The drag and drop exercise below orders the lookup that produced the log line.
How to answer
Ordering exercise with 5 items. Move them into the correct order, then check your answer.
Order the classes that Python checks when JsonWithMixins.render runs, from first to last.
7. Composition and the Liskov Test
The Liskov substitution principle says that a subclass must be usable wherever the base class is expected, without surprising the caller. The mutable square is the classic counterexample, because a square inherits width and height setters that it cannot honour independently.
class Rectangle:
def __init__(self, width: float, height: float) -> None:
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
class Square(Rectangle):
def __init__(self, side: float) -> None:
super().__init__(side, side)
@property
def side(self) -> float:
return self.width
@side.setter
def side(self, value: float) -> None:
self.width = value
self.height = value
square = Square(4)
print(square.area())
square.width = 8
print(square.area(), square.height)The output is 16, then 32 4. After assigning width, the square is no
longer a square: its width is 8 and its height is 4. Any function that accepts
a Rectangle and sets width expects the height to survive, and the subclass
violated that expectation. The area method is correct; the inheritance
relationship is not.
Composition fixes the model. A square becomes a distinct type with a single
side, and the existing Rectangle keeps both dimensions.
class SquareShape:
def __init__(self, side: float) -> None:
self.side = side
def area(self) -> float:
return self.side ** 2Both classes now satisfy the same implicit interface, and a caller that only
needs area accepts either. Nothing claims that one is the other. The shared
behaviour is a method signature, which is exactly what a Protocol or duck
typing describes.
The quadrant below places the four tools by coupling and flexibility; the positions are a judgement, not a measurement.
Description
A quadrant chart placing inheritance at high coupling and low flexibility, composition at low coupling and high flexibility, mixins at medium coupling and medium flexibility, and protocols at low coupling and medium flexibility.
Inheritance sits in the rigid, coupled corner because a base class change propagates and the relationship is fixed at class creation. Composition sits in the flexible, decoupled corner because the collaborator can be replaced at runtime. Mixins sit in the middle, more reusable than a deep base class and more coupled than a held object. Protocols are decoupled but only moderately flexible, because they describe a shape without providing code.
8. Real-World Case Studies
pathlib splits pure logic from platform behaviour
The pathlib module defines PurePath for syntax that works on any operating
system, and Path for the concrete class that touches the file system. The
concrete class is further split into PosixPath and WindowsPath, chosen at
import time for the host platform.
from pathlib import PurePosixPath, PureWindowsPath
posix = PurePosixPath("/srv/reports/2026/q4.csv")
windows = PureWindowsPath(r"C:\reports\2026\q4.csv")
print(posix.name, posix.suffix, posix.parent.name)
print(windows.drive, windows.name, len(windows.parts))The output is q4.csv .csv 2026, then C:\ q4.csv 4. Both objects run the
same pure parsing code, and neither touches a disk. The concrete Path adds
the operations that need an operating system, and the split keeps the parsing
logic testable everywhere.
The lesson is that inheritance is used for a stable contract, not for code reuse alone. The pure class defines the interface, the concrete classes supply platform behaviour, and the platform choice is a factory decision the caller never writes.
Django composes views from mixins
Django's generic class-based views are assembled from mixins such as
SingleObjectMixin and TemplateResponseMixin. A DetailView combines
several of them, and the documentation explains that method resolution order
decides which method runs when two mixins define the same name.
Its examples override get_context_data, call super() and add one key to the
result, which is the cooperative pattern from section 6 applied inside a
framework.
Django also warns that mixins from the detail and list families should not be
combined casually, because their get_context_data and queryset behaviour
overlap in ways the MRO cannot resolve for you.
The comparison is the point. pathlib uses a small, deliberate hierarchy with
one axis of variation, the platform. Django uses mixins for orthogonal features
and pays for it with ordering rules. Neither is wrong; each matches the shape
of the problem.
9. When Not to Write a Subclass
A hierarchy that starts as three classes tends to grow a fourth and a fifth, and the base class slowly collects flags for each child. The warning signs are easy to name.
- The base class checks
isinstanceor a type flag to decide what to do. - A subclass overrides a method only to raise
NotImplementedError. - Two subclasses share code that the base class does not use.
- A change to one subclass requires a matching change in a sibling.
- The is-a sentence needs the word "kind of" twice to sound true.
When two or more of those signs appear, the hierarchy is encoding a set of options, not a type. A strategy object or a dictionary of functions usually expresses the same design with less coupling, at the cost of a named class that documents the behaviour. That trade is worth it for local variation and not for a public plugin API.
Do not use inheritance to share a single helper method. A module level function or a small composed object does the same job without binding two classes together for the life of the codebase.
2026 Updates (as of November 2026)
- Python 3.15.2 is the current stable documentation set.
typing.overridehas been available since Python 3.12 and is now supported by the major type checkers, so a renamed base method fails static analysis instead of silently forking behaviour.- Protocols and structural subtyping are part of the standard typing
vocabulary, not a third-party experiment, and
runtime_checkableis the documented bridge toisinstance. - Abstract base classes remain the tool for contracts you own, with virtual
subclass registration and
__subclasshook__unchanged in the 3.15 documentation.
π Did you know?
- The zero argument form of
super()works because the compiler adds a hidden__class__cell to every method that mentions it, so the proxy knows where to resume even after the class is renamed. objectappears at the end of every MRO, andtypeis an instance of itself, which is whytype(object)istype.- An ABC registered with
registeranswersisinstanceandissubclassaffirmatively, yet its methods never enter the MRO, so inherited code is not shared.
Comparative Verdict. Use inheritance when the subtype genuinely passes every base class promise and the contract is stable. Use a mixin when one orthogonal behaviour must be layered across unrelated classes, and keep the chain cooperative. Use a Protocol when the implementers are outside your control or must not inherit. Use composition when the relationship is has-a, when the collaborator may be swapped at runtime, or when the subclass would need to weaken the base contract.
Practice Questions
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
Classes A, B and C are defined so that B inherits from A, C inherits from A, and D inherits from B then C. D defines no label method, while A, B and C each define label. What does D().label() return, and what is the MRO?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
In a class Leaf(Left, Right), both Left and Right define __init__ and call super().__init__(**kwargs). What does super() refer to inside Left.__init__?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
An abstract base class declares one abstract method, area, and no subclass implements it. What happens when the base class itself is instantiated?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A third-party class already has a compatible render method but cannot inherit from your base class. You want both static checking and a runtime isinstance check. Which approach fits?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A view class is declared as class View(LoggingMixin, RetryMixin, JsonExporter), and all four classes define render. Which render implementation runs first?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
Square inherits from Rectangle and overrides the side property so that setting width also sets height. A function accepts a Rectangle, sets width to 8, and expects height to stay unchanged. Why is this a Liskov violation?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
What is the difference between isinstance(obj, Base) and issubclass(Sub, Base)?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
An ABC defines __subclasshook__ to return True for any class with a render method. A class with that method is checked with issubclass and the answer is True. What does the class gain?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A base class renames method save to persist. A subclass still defines save and nothing calls it. Which tool turns this silent fork into an error before deployment?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A base class declares registry: list[str] = [] and subclasses append their names in __init__. Every subclass sees every name. Which fix keeps one registry per subclass?
Key Takeaways
- Inheritance is a substitutability promise, not a description of the real world. If a subclass cannot honour every base behaviour, use composition.
super()means the next class in the instance MRO, which is what makes cooperative multiple inheritance run each parent exactly once.- C3 linearization preserves local precedence and monotonicity, and an inconsistent hierarchy fails at class creation rather than at call time.
- Abstract base classes enforce contracts at instantiation, while Protocols describe a shape for classes you do not own.
- Mixins add orthogonal behaviour but make ordering part of the program; keep them stateless and cooperative.
typing.overrideturns a renamed base method into a static error, which is the cheapest defence against silent forks.- Prefer the shallowest hierarchy that passes the is-a test, and remember that a dictionary of strategies often beats a fourth subclass.
References
Every factual claim links to its source, with the access date printed so the source can be rechecked.