Name the inheritance shape before tracing it
Classify single, multilevel, hierarchical and diamond inheritance before comparing Java and Python rules. Inheritance and polymorphism · Object-oriented programming.
A payment app has a CardPayment and a WalletPayment, both built on one Payment, and a RefundableCardPayment that wants to build on both. If both parents offer a refund(), which one runs? The answer depends on the language, so first name the shape the parent edges draw.
One edge is single inheritance. A row of edges, each class with one parent, is a chain, called multilevel inheritance. Sibling classes that share one parent are hierarchical inheritance. Two branches that meet again below a shared ancestor form a diamond: the joining type has two parents, and both lead back to the same ancestor. Two parents alone do not make a diamond, because a class whose parents never meet at a shared ancestor is multiple inheritance but not a diamond. The word names the picture; it does not promise one object layout or one lookup result.
Name the shape as each parent edge appears
Read the steps as text
- Nothing is declared yet. Each class that names a parent adds an edge. The label is read from the edges drawn so far, so an added edge can change it.
- Left names Root as its parent. One parent edge is single inheritance.
- Join names Left as its parent. Each class still has one parent and the edges run in a chain, which is multilevel inheritance.
- Right names Root as its parent. Root now has two direct children, Left and Right, and each class still has one parent, which is hierarchical inheritance.
- Join also names Right as a parent. Join now has two parent edges, so the earlier label no longer fits.
- Join's two parents, Left and Right, both lead back to Root, so the edges form a diamond.
Java: one superclass, several interface contracts
Java 17 and later give each class at most one direct superclass; Object is the root class with none. A class may also implement several interfaces, and an interface may extend several interfaces. That lets Java draw a diamond among Root, Left, Right and Join as a type and contract graph without implying two superclass objects.
The extends edges between interfaces describe inherited contracts. The implements edges from Join describe capabilities that the class promises. Base is the one direct class superclass in the fixed example, and it declares no f() method.
Here is a complete Java/Python pair. The Java side makes the default-method boundary explicit: Left.f() and Right.f() are unrelated defaults, while Join.f() supplies the resolving declaration. The Python side uses the same names to show a valid multiple-inheritance declaration, while keeping its lookup rule separate.
interface Root { }
interface Left extends Root {
default String f() { return "left"; }
}
interface Right extends Root {
default String f() { return "right"; }
}
class Base { }
final class Join extends Base implements Left, Right {
@Override public String f() { return Left.super.f(); }
}class Root:
def f(self):
return "Root"
class Left(Root):
def f(self):
return "Left>" + super().f()
class Right(Root):
def f(self):
return "Right>" + super().f()
class Join(Left, Right):
def f(self):
return "Join>" + super().f()Without the override, class Join implements Left, Right { } does not compile: javac reports that Join inherits unrelated defaults for f(). The override can still reuse one parent: inside Join.f(), Left.super.f() calls Left’s default, and Right.super.f() calls Right’s. Java never picks the first interface listed.
The conflict needs two unrelated defaults. If interface Sub extends Left overrides f(), then in class J2 implements Left, Sub the more specific Sub.f() wins and nothing conflicts. If the superclass Base has a concrete f(), that method wins over both defaults.
Java: two interface defaults, and when they conflict
Read the steps as text
- Two small programs follow. In each, a class implements interfaces that declare a default f(), and Java must decide which default runs.
- Left and Right are interfaces that extend Root and each declares a default f().
- Join is a class that implements Left and Right.
- The compiler collects the default f() from Left and Right. Neither interface extends the other, so neither is more specific, and Join does not compile.
- Join declares its own f(), so the conflict clears. Inside it, Left.super.f() chooses which default to reuse.
- Running new Join().f() reaches Join's own f(), which hands over to Left's default and returns "left".
- In the second program, Sub is an interface that extends Left and declares a default f(). J2 is a class that implements Left and Sub.
- The compiler collects the default f() from Left and Sub. Sub extends Left, so its f() is more specific and Left's drops out. Nothing conflicts.
- Running new J2().f() uses Sub's default f() and returns "sub".
Python: the order is part of the contract
Python 3 permits class Join(Left, Right) when the hierarchy admits a consistent C3 method resolution order. For this fixed declaration order, the visible search order is Join, Left, Right, Root, object. Python builds that list by three rules. A class comes before its parents. Bases keep the order written in the class statement, so Left comes before Right. A shared ancestor comes only after every class that inherits from it, so Root waits until both Left and Right are placed. That is why lookup does not go depth-first to Root after Left.
The list is an ordered lookup rule, not a diagram of five stored object bodies. If an attribute is absent on Join, lookup checks the next class in that order. Some base orders have no consistent list. If B inherits from A, then class X(A, B) raises TypeError when the class is created, because A would have to come both before and after B. Writing class X(B, A) works.
Each body forwards with super().f(), so the fixed receiver Join() produces "Join>Left>Right>Root" along the displayed MRO. Notice that Left calls super().f() and lands in Right, a sibling it does not inherit from. super() continues after the current class in the MRO; it does not mean “call the textual parent next to this class,” and here the next class in Join’s MRO is Right, not Root. Root runs once, after both branches.
Python: build the order, then walk it
Read the steps as text
- A class statement with two bases is about to be declared. Python has to turn its base list into one fixed search order.
- Left derives from Root. Right derives from Root. Join lists Left first and then Right.
- A class always comes before its parents, so C3 places Join first. Left is the next base Join lists and nothing else holds it back, so it goes next.
- Root could go next, but Right still has to come before it, so Root waits and Right is placed.
- Root follows because every class that inherits from it is now placed. Every list ends with object, so the finished order is Join, Left, Right, Root, object.
- Calling f() on a Join object starts at the first class in the order, so Join's f() runs and then calls super().
- Inside Join, super() continues with the next class in the order, Left, and its f() runs.
- Inside Left, super() continues with the next class in Join's order, which is Right, a class Left does not inherit from.
- Inside Right, super() continues with the next class in the order, Root, and its f() runs. The call returns "Join>Left>Right>Root", so each class ran once.
Practical rule: prefer small interfaces or composition to deep multiple inheritance. When you do use a Python diamond, make every class call super().f() with the same signature so each class runs once.
Recap
- Single, multilevel, hierarchical and diamond shapes describe different parent-edge patterns.
- Two parents alone do not make a diamond, and Java is not free of the problem: two interface defaults can conflict.
- Java has one superclass but many interfaces; two unrelated defaults with the same signature must be resolved by the class, optionally with
Left.super.f(). - Python’s consistent C3 MRO makes lookup order explicit, and
super()follows that order.
Go deeper
Java: abstract-method agreement is not a default conflict. Python: super() with no arguments only works inside a method, and a mixin that skips super() breaks the cooperative chain.
You have completed the Inheritance and polymorphism module; continue to object relationships to move from what a class inherits to which objects hold links to, share or own one another. Practise reading parent edges and paths in Graph Fundamentals.