Polymorphism and Class Hierarchies
Substitution works because the derived object supplies the behavior behind the base interface. The caller depends on the base class. The specific derived class is what it receives at run time.
Writing Code Against a Base Class
Inheritance lets one class build on another. A derived class takes the structure of a base class and adds or changes behavior. Most introductions stop at the mechanics of writing a derived class: how to declare it, how to add methods, and how to change what an inherited behavior does. Interface inheritance asks a different question. Once a derived class exists, where can it be used? The answer to that question is the central idea of this article, and it changes how you read a class hierarchy.
Keep one question in mind as you read. Suppose some code is written to expect an object of a base class. What happens when you hand it an object of a derived class instead? The rest of this article follows that question. It first states the rule, then traces what the receiving code actually depends on, and finally shows how abstract base classes usually fit into the picture.
The Substitution Rule
An object of a derived class can be used wherever an object of a base class is required.
This rule is sometimes called substitution. A derived object can stand in for a base object at the point where the base object is expected. The rule describes what is allowed at that point. It does not describe how the object was built or what it looks like internally.
The source explains the consequence of this rule directly: the base class acts as an interface for the derived class. In this context, the word interface means the set of things a caller is allowed to rely on. When code works with a base class, it expects certain operations to exist and to behave in a certain way. A derived class that honors that expectation can be substituted in, and the caller does not need to know which specific derived class it received.
Substitution works because the derived object supplies the behavior behind the base interface. The caller depends on the base class. The specific derived class is what it receives at run time.
Think about what a caller needs in order to do its job. It usually does not need to know how an object is built internally. It needs to know which operations it can request and what those operations will return. The base class presents exactly that surface. The derived class sits behind the same surface and supplies its own version of the work.
A Report That Accepts Any Shape
The source names a Shape idea, and the example below follows it. A base class called Shape defines the operation that callers use, which is area. A derived class called Square provides the concrete behavior for that operation. The function report is written to accept any Shape, so it is written against the base class. This example is an original illustration written for this article. The source itself does not contain this code.
Area: 9- Shape defines the operation that callers are allowed to use.
- Square is a derived class that supplies the concrete behavior behind area.
- report is written against Shape, not against Square.
- The call
report(Square(3))passes a derived object where a base object is expected.
What do you think happens?
Before reading on, what do you think report(Square(3)) prints, and why does report not need a special case for Square?
Reveal answer
It prints Area: 9. The side is 3, so area returns 3 times 3, which is 9.
report calls shape.area() and trusts that the object it received supplies that operation. Square supplies it, so the call succeeds without report checking the exact class.
Notice what report never does. It never checks whether it received a Square. It only asks for the area operation that the base interface promises. If you later add a Circle class that also derives from Shape, report can accept it without any change to report itself. That is the practical payoff of the rule. The function was written once against the base class and keeps working as new derived classes are added.
Following One Call Through the Interface
Substitution is the moment when a derived object arrives where a base object was expected. The sequence below follows a single request through that moment. The caller is written against the base interface, the derived object is supplied, and the derived class provides the behavior that the base interface promises. The diagram shows the order of the messages, and the list after it walks through each step in words.
- 1The call
report(Square(3))passes a Square object into the parameter named shape. - 2Inside report, the expression
shape.area()asks for the operation the Shape interface promises. - 3The Square object supplies the behavior behind that interface, so its area method runs with side equal to 3.
- 4The result 9 returns to report, which prints it. At no point does report inspect the exact class of its argument.
The caller's code stays the same whether the object it holds is a base class object or a derived one. Only the behavior behind the interface changes. This is why the rule says a derived object can be used wherever a base object is required, rather than making any claim about the caller's internals.
The Base Class as a Contract
Read through a base-typed reference, a derived object exposes only what the base class declares. The caller is written against the base class, so it can reach the operations in the base interface and nothing more. Members that exist only on the derived class are not part of the surface the caller depends on.
In the diagram, the caller depends on Shape. The Square object also has a side attribute, which is part of its own structure. Through a Shape-typed reference, report can reach area and cannot use side. That restriction is not a flaw. It is the reason the base class works as an interface: the caller relies only on the operations the base class promises.
The direction of trust matters. The caller is written against the base class, and the derived class is what it receives at run time. The caller should never need to know which derived class is present.
Where the Hierarchy Branches
A single base class can have several derived classes. Each one is a different way of providing the same base interface. The shared operations are declared once in the base, and each derived class supplies its own behavior for them. The diagram below shows Shape with two derived classes, Square and Circle. Circle is the example the source mentions as a possible addition, and it is drawn here to show branching.
When you read a hierarchy like this, look at two things. The base class tells you which operations every member of the family offers. Each derived class tells you how that family member fulfills them. A function written against Shape can accept any node below Shape in this tree, because each of them honors the same interface.
Abstract Base Classes in Interface Inheritance
The source notes that the classes used as base classes in interface inheritance are often abstract classes. An abstract class is a base class meant to define an interface rather than to be used directly as a finished object. Its job is to say which operations exist, so that derived classes can supply the concrete versions. In the Shape example, the base class declares area, and each derived class is responsible for making that operation real.
The version below uses Python's abc module to make Shape abstract. This is an original illustration for this article. It is one common way to express the same idea, and it is not presented as the only way.
Area: 9- Shape is abstract: area is declared but its behavior is left to derived classes.
- Square must implement area to be a complete, usable derived class.
- report still depends only on the interface, so its code is unchanged from the earlier version.
Mistakes When Reading Hierarchies
Common mistake Thinking substitution means the caller can use every member of the derived class
What it looks likeInside report, writing shape.side when shape is typed as Shape
Why it failsThe caller is written against the base interface. Members that exist only on the derived class are not part of what the caller is allowed to rely on.
Do this insteadCall only the operations the base class declares, such as area, through the base-typed reference.
Common mistake Adding a check for the exact derived class inside a function that takes the base class
What it looks likeWriting if isinstance(shape, Square) inside report before calling area
Why it failsThe point of the rule is that report does not need to know which derived class it received. A special case undoes the flexibility the base interface provides.
Do this insteadRely on the base interface. If a new derived class honors the interface, report should accept it without modification.
Common mistake Assuming a derived class can ignore the base interface
Why it failsSubstitution works only because the derived object supplies the behavior behind the base interface. A derived class that does not honor that expectation breaks the rule.
Do this insteadA derived class should provide the operations the base promises, behaving as the base interface expects.
Common mistake Treating a hierarchy as a list of unrelated classes
Why it failsThe base class is what makes the derived classes a family. Reading the branches without the base misses which operations they share.
Do this insteadStart from the base class to see the shared interface, then look at each derived class for its own behavior.
Practice Tracing a Substitution
Using the Shape, Square, and report code from earlier, imagine you add a new derived class called Circle that also derives from Shape and supplies its own area. Without changing report, trace what happens when report(Circle(1)) is called. Name the interface report relies on, identify which object supplies the behavior, and say whether report needed any change.
Hints
- Ask which operation report calls on its parameter.
- Ask which class's area method runs when the object passed in is a Circle.
- Remember that report is written against Shape, not against Square or Circle.
Key Takeaways
- An object of a derived class can be used wherever an object of a base class is required. This is the substitution rule.
- The base class acts as an interface: callers rely on the operations it promises and do not need to know which derived class they received.
- Substitution works because the derived object supplies the behavior behind the base interface, so the calling code keeps the same form.
- Abstract classes are often the base classes used in interface inheritance, because they declare what operations exist while derived classes supply the concrete versions.