Inheritance and Polymorphism
Intermediate⏱75 minutesπŸ“–17 min readLesson 2 of 10

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

Base class
A class that another class inherits from, also called a parent or superclass.
Subclass
A class that inherits attributes and methods from one or more base classes.
Override
A method in a subclass that replaces the implementation found earlier in the MRO.
MRO
The method resolution order, a linear list of classes that Python searches from most specific to object.
C3 linearization
The algorithm that builds the MRO while preserving local precedence and monotonicity.
Cooperative inheritance
A design where every class in a multiple inheritance chain calls super() so each parent runs once.
Abstract base class
A class that declares methods a subclass must implement and cannot be instantiated until it does.
Protocol
A typing construct that describes required attributes and methods, so any matching class qualifies without inheriting.
Mixin
A small class that adds one behaviour to a hierarchy and is not meant to be instantiated on its own.
Composition
A design where an object holds other objects and delegates work to them instead of inheriting their behaviour.
Liskov substitution
The rule that a subclass must be usable anywhere its base class is expected without breaking callers.

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.

python
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.

python
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.

100%
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.

⚠️Warning

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.

python
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.

100%
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.

πŸ’‘Tip

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.

Corey SchaferWatch on YouTube
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.

Matching Question
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.

Method override
Drop here
super()
Drop here
Abstract method
Drop here
Virtual subclass
Drop here
@override
Drop here

Drag answers from here:

The subclass implementation runs instead of the base one
The search continues after the current class in the MRO
Instantiation fails until a subclass implements it
issubclass returns True without an inheritance edge
A type checker flags a method that overrides nothing

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.

python
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.

python
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.

100%
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.

python
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.

πŸ“ŒImportant

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.

Fill in the Blanks
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:

MRO
C3
super()
TypeError
depth first search
isinstance
object
AttributeError

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.

python
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.height

The class cannot be instantiated while an abstract method is unimplemented. The console session below shows the exact failure and the working subclass.

python
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.

python
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.

ℹ️Note

Virtual subclasses do not appear in the MRO, and methods defined on the ABC are not callable through them. Registration is a statement about identity, not a way to inject code.

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.

python
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.

python
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
πŸ“ŒImportant

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.

python
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.

100%
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.

⚠️Warning

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.

Ordering Question
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.

JsonWithMixins
LoggingMixin
RetryMixin
JsonExporter
Exporter

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.

python
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.

python
class SquareShape: def __init__(self, side: float) -> None: self.side = side def area(self) -> float: return self.side ** 2

Both 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.

100%
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.

python
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 isinstance or 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.

🚨Danger

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.override has 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_checkable is the documented bridge to isinstance.
  • 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.
  • object appears at the end of every MRO, and type is an instance of itself, which is why type(object) is type.
  • An ABC registered with register answers isinstance and issubclass affirmatively, yet its methods never enter the MRO, so inherited code is not shared.
πŸ“ŒImportant

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

Practice Question
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?

Practice Question
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__?

Practice Question
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?

Practice Question
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?

Practice Question
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?

Practice Question
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?

Practice Question
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)?

Practice Question
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?

Practice Question
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?

Practice Question
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?

βœ…Success

Key Takeaways

  1. Inheritance is a substitutability promise, not a description of the real world. If a subclass cannot honour every base behaviour, use composition.
  2. super() means the next class in the instance MRO, which is what makes cooperative multiple inheritance run each parent exactly once.
  3. C3 linearization preserves local precedence and monotonicity, and an inconsistent hierarchy fails at class creation rather than at call time.
  4. Abstract base classes enforce contracts at instantiation, while Protocols describe a shape for classes you do not own.
  5. Mixins add orthogonal behaviour but make ordering part of the program; keep them stateless and cooperative.
  6. typing.override turns a renamed base method into a static error, which is the cheapest defence against silent forks.
  7. 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.

  1. Python Software Foundation Β· Accessed 2026-11-06

  2. Python Software Foundation Β· Accessed 2026-11-06

  3. Python Software Foundation Β· Steven Troxler, Joshua Xu, Shannon Zhu Β· Accessed 2026-11-06

  4. Python Software Foundation Β· Michele Simionato Β· Accessed 2026-11-06

  5. Python Software Foundation Β· Accessed 2026-11-06

  6. Python Software Foundation · Ivan Levkivskyi, Jukka Lehtosalo, Łukasz Langa · Accessed 2026-11-06

  7. Django Software Foundation Β· Accessed 2026-11-06

  8. Python Software Foundation Β· Antoine Pitrou Β· Accessed 2026-11-06

  9. Python Software Foundation Β· Accessed 2026-11-06

  10. Python Software Foundation Β· Accessed 2026-11-06

Progress20%