Shape Calculator
The Inheritance Playground
A comprehensive exploration of Python inheritance through geometry. Beyond the basic Shape hierarchy,
this guide covers Method Resolution Order, super() mechanics, composition vs inheritance,
and real-world design decisions — all with live, runnable code.
Why Inheritance Matters in Real-World Python
Inheritance is one of the most powerful — and most misunderstood — tools in object-oriented programming. At its core, inheritance is about code reuse and type relationships. But in practice, it's also about architectural clarity, maintainability, and extensibility. When used correctly, inheritance turns a chaotic collection of functions into a coherent, self-documenting system.
🧬 The Shape Hierarchy as a Teaching Vehicle
The shape hierarchy — Shape as a base, with Circle, Rectangle,
Square, and Triangle as subclasses — is a classic teaching example for
good reason. It's simple enough to grasp immediately, yet it contains all the key elements of
inheritance: a shared interface, specialized implementations, and a clear "is-a" relationship.
But the real learning comes when you push beyond the basics.
📐 What Makes This Guide Different
Most tutorials stop after showing you how to write class Circle(Shape): and override
area(). This guide goes much further. You'll learn:
- → How Python's Method Resolution Order (MRO) actually works
-
→ The difference between
super()in single vs multiple inheritance - → When to use inheritance vs composition — with concrete examples
- → How to design hierarchies that don't break when requirements change
- → Abstract base classes and mixins — advanced inheritance patterns
🎯 Who This Is For: This guide is for developers who already understand the basics of Python classes and want to truly master inheritance. If you've ever wondered why your super() call didn't work as expected, or when to use inheritance vs composition, this is for you.
Understanding How Python Really Does Inheritance
🔍 The Method Resolution Order (MRO)
When you call a method on an instance, Python needs to decide which method to execute. In single inheritance, this is straightforward: search the child class, then the parent, then the grandparent, and so on. But with multiple inheritance, the order is more complex.
Python uses the C3 linearization algorithm to compute a consistent, monotonic
MRO. You can see it with ClassName.__mro__. The MRO guarantees that a class always
appears before its parents, and that the order is predictable across all classes in the hierarchy.
Key Insight: The MRO is not just a list of classes — it's the search path.
When you call super().method(), you're calling the next method in the MRO,
not necessarily the parent class's method. This is why super() works so elegantly
in multiple inheritance — it's MRO-aware.
class A: pass class B(A): pass class C(A): pass class D(B, C): pass print(D.__mro__) # (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
- MRO Order: D → B → C → A → object. Python searches D first, then B, then C, then A, then the base object class.
- Why B before C? Because D declares
(B, C)— the left-to-right order determines priority. - Why A last? Even though both B and C inherit from A, C3 ensures A is visited only once, after both B and C have been checked.
- Why this matters: If B and C both define a method, the version from B will be used. If only C defines it, C's version will be used. If neither defines it, A's version is used.
🔧 super() — More Than Just a Parent Call
In most languages, super is a hard-coded reference to the immediate parent class.
In Python, super() is far more dynamic. It returns a proxy object that delegates
method calls to the next class in the MRO — which might not be the parent at all.
This is what makes super() so powerful in cooperative multiple inheritance.
It ensures that in a diamond hierarchy (A → B, A → C, D → B, C), every class's method gets
called exactly once, in the correct order, even if not every class explicitly calls super().
class Shape: def __init__(self, color: str): self.color = color print(f"Shape __init__: color={color}") class ColoredCircle(Circle, Shape): def __init__(self, radius: float, color: str): super().__init__(radius) super(ColoredCircle, self).__init__(color) # MRO: ColoredCircle → Circle → Shape → object
⚠️ Common Mistake: Calling super().__init__() multiple times with different arguments can lead to confusing errors. In cooperative multiple inheritance, all classes should have compatible __init__ signatures, or use keyword arguments with **kwargs to pass through unknown parameters.
⚖️ Inheritance vs Composition — The Eternal Debate
"Favor composition over inheritance" is one of the most cited principles in OOP design. But what does it actually mean? And when should you still use inheritance?
Inheritance is an "is-a" relationship: a Dog is an Animal. Composition is a "has-a" relationship: a Car has an Engine. Inheritance is for specializing behavior; composition is for assembling capabilities.
✅ Use Inheritance When
- • There's a clear "is-a" relationship
- • You need to override behavior
- • You want polymorphism across a family of types
- • The hierarchy is stable and unlikely to change
✅ Use Composition When
- • There's a "has-a" relationship
- • You want to reuse behavior without creating a type relationship
- • The hierarchy is likely to change frequently
- • You need to combine behaviors from many sources
# Inheritance: Shape → Circle (is-a) class Circle(Shape): pass # Composition: DrawingTool uses a Shape (has-a) class DrawingTool: def __init__(self, shape: Shape): self.shape = shape # Composition: Window has a list of Shapes class Canvas: def __init__(self): self.shapes = [] def add(self, shape: Shape): self.shapes.append(shape)
💡 Pro Tip: In practice, many systems use both. Django models use inheritance for base model classes, but use composition for fields and managers. A well-designed system uses the right tool for the right job.
🏗️ Abstract Base Classes & Mixins — Advanced Patterns
Python's abc module lets you define abstract base classes that enforce
a contract on subclasses. This is useful when you want to ensure that every subclass implements
certain methods. Mixins are small classes that provide reusable behavior to be mixed into
other classes via multiple inheritance.
from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self) -> float: pass def describe(self) -> str: return f"{type(self).__name__} area = {self.area():.2f}" class JsonMixin: def to_json(self) -> str: return f'{{"type": "{type(self).__name__}", "area": {self.area()}}' class Circle(Shape, JsonMixin): def __init__(self, radius: float): self.radius = radius def area(self) -> float: return 3.14159 * self.radius ** 2 c = Circle(5) print(c.describe()) print(c.to_json())
Shape Class Hierarchy — Full Implementation
This is the complete shape hierarchy. Notice how describe() is defined once in Shape and works for every subclass — that's polymorphism in action.
import math class Shape: def area(self) -> float: raise NotImplementedError("Subclasses must implement area()") def describe(self) -> str: return f"{type(self).__name__} area = {self.area():.2f}" class Circle(Shape): def __init__(self, radius: float): self.radius = radius def area(self) -> float: return math.pi * self.radius ** 2 class Rectangle(Shape): def __init__(self, width: float, height: float): self.width = width self.height = height def area(self) -> float: return self.width * self.height class Square(Rectangle): def __init__(self, side: float): super().__init__(width=side, height=side) class Triangle(Shape): def __init__(self, base: float, height: float): self.base = base self.height = height def area(self) -> float: return 0.5 * self.base * self.height shapes = [ Circle(5), Rectangle(4, 6), Square(3), Triangle(8, 5) ] for s in shapes: print(s.describe()) print(f"Square is subclass of Rectangle? {issubclass(Square, Rectangle)}") print(f"Circle is subclass of Shape? {issubclass(Circle, Shape)}")
- Base class
Shape— defines the common interface.area()raisesNotImplementedErrorto enforce that subclasses provide their own implementation. describe()is inherited unchanged — every subclass can call it. It dynamically callsself.area(), which resolves to the correct subclass method at runtime.Squareinherits fromRectangle— it reusesRectangle.__init__viasuper(), demonstrating multilevel inheritance.- Polymorphism in action — the
forloop treats every shape identically, yet each produces the correct area. - Type introspection — the
issubclass()checks confirm the inheritance relationships at runtime.
Interactive Shape Calculator
Modify the code below — change dimensions, add new shapes, or experiment with inheritance. Click Run to see the output. This is your sandbox for exploring inheritance.
import math class Shape: def area(self) -> float: raise NotImplementedError("Subclasses must implement area()") def describe(self) -> str: return f"{type(self).__name__} area = {self.area():.2f}" class Circle(Shape): def __init__(self, radius: float): self.radius = radius def area(self) -> float: return math.pi * self.radius ** 2 class Rectangle(Shape): def __init__(self, width: float, height: float): self.width = width self.height = height def area(self) -> float: return self.width * self.height class Square(Rectangle): def __init__(self, side: float): super().__init__(width=side, height=side) class Triangle(Shape): def __init__(self, base: float, height: float): self.base = base self.height = height def area(self) -> float: return 0.5 * self.base * self.height shapes = [ Circle(7), Rectangle(5, 8), Square(4), Triangle(10, 6) ] for s in shapes: print(s.describe())
✏️ Try it yourself: Change the numbers in the shapes list. Hit Run to see your changes live. Experiment with adding a Pentagon class or overriding describe() for a specific shape.
Reinforce Your Understanding
Work through these exercises to solidify your grasp of inheritance. Each exercise builds on the shape hierarchy and introduces a new concept.
📝 Exercise 1: Add a New Shape — Pentagon
Create a Pentagon class that inherits from Shape. Its area formula is:
area = 1.72048 × side². Add an instance to the shapes list and run the code.
super().__init__() if you add a custom __init__. Since Shape.__init__ does nothing, you can skip it, but it's good practice to call it anyway.
class Pentagon(Shape): def __init__(self, side: float): super().__init__() self.side = side def area(self) -> float: return 1.72048 * self.side ** 2 shapes.append(Pentagon(3))
📝 Exercise 2: Override describe() for Circle
Override the describe() method in Circle to include the radius in the description.
Expected output: "Circle (r=5) area = 78.54"
super().describe() to get the parent's output, but since the parent uses type(self).__name__, you might want to build the string from scratch for clarity.
class Circle(Shape): def __init__(self, radius: float): self.radius = radius def area(self) -> float: return math.pi * self.radius ** 2 def describe(self) -> str: return f"Circle (r={self.radius}) area = {self.area():.2f}"
📝 Exercise 3: Add a Perimeter Method
Add a perimeter() method to the Shape class that raises NotImplementedError.
Implement it in Circle, Rectangle, and Triangle. For Triangle, assume it's equilateral for simplicity.
2 * π * r. Rectangle: 2 * (w + h). Triangle: 3 * base (equilateral). Don't forget to update the shapes list to test your new method.
class Shape: def perimeter(self) -> float: raise NotImplementedError("Subclasses must implement perimeter()") class Circle(Shape): # ... __init__ and area ... def perimeter(self) -> float: return 2 * math.pi * self.radius class Rectangle(Shape): # ... __init__ and area ... def perimeter(self) -> float: return 2 * (self.width + self.height) class Triangle(Shape): # ... __init__ and area ... def perimeter(self) -> float: return 3 * self.base # equilateral # Test: for s in shapes: print(f"{type(s).__name__} perimeter = {s.perimeter():.2f}")
📝 Exercise 4: Multiple Inheritance — Rhombus
Create a Rhombus class that inherits from both Rectangle and a new DiagonalMixin mixin that provides a diagonals() method. The rhombus area = (d1 × d2) / 2.
super() carefully in the constructor. The mixin should have no __init__ to avoid conflicts. Override area() to use the diagonal formula.
class DiagonalMixin: def diagonals(self) -> tuple: return (self.d1, self.d2) class Rhombus(Rectangle, DiagonalMixin): def __init__(self, d1: float, d2: float): self.d1 = d1 self.d2 = d2 # width and height not directly used def area(self) -> float: return (self.d1 * self.d2) / 2 rhombus = Rhombus(6, 4) print(rhombus.describe()) print(f"Diagonals: {rhombus.diagonals()}")
Common Questions About Inheritance
Why use inheritance instead of just copying code?
+Inheritance centralizes shared logic in one place. If you copy-paste, a bug fix or change must be repeated in every copy. With inheritance, you update the parent once and all children benefit automatically. It also establishes a clear "is-a" relationship that makes your code more intuitive and self-documenting.
What's the difference between inheritance and composition?
+Inheritance is an "is-a" relationship: a Dog is an Animal. Composition is a "has-a" relationship: a Car has an Engine. Inheritance is for specializing behavior; composition is for assembling capabilities. Use inheritance when you genuinely have a subtype; use composition when you just need to reuse functionality without creating a type hierarchy.
What is the super() function, and why use it?
+
super() returns a proxy object that delegates method calls to the next class in the Method Resolution Order. It's essential in __init__ to ensure the parent's initialization runs. It also makes overrides extend rather than replace — you can call super().method() to run the parent's logic and then add your own. In multiple inheritance, super() ensures cooperative method calls across all classes in the hierarchy.
Can a class inherit from multiple parent classes?
+Yes — Python supports multiple inheritance: class C(A, B):. The Method Resolution Order determines which parent is searched first. Use it sparingly; it can make code harder to reason about. Mixins (small, focused classes that provide reusable behavior) are a common and safe use of multiple inheritance.
What is the Method Resolution Order (MRO)?
+MRO is the order in which Python searches for a method or attribute in a class hierarchy. You can view it with ClassName.__mro__. Python uses the C3 linearization algorithm to compute a consistent, predictable order even with complex multiple inheritance. The MRO guarantees that a class appears before its parents, and that the order is monotonic — it never changes based on where you start the search.
When should I use abstract base classes (ABCs)?
+Use ABCs when you want to define a contract that all subclasses must fulfill. An ABC with abstract methods cannot be instantiated directly, and any subclass that doesn't implement all abstract methods will raise an error at instantiation time. ABCs are great for frameworks and libraries where you want to enforce a consistent interface across many implementations. They're also useful for documenting the expected behavior of a class hierarchy.
Explore the Full OOP Series
Inheritance is the second of four interconnected pillars. Each one builds on the last. Explore the complete journey.
Bundle data with methods. Control access. Protect state. The guardian of object integrity.
ExploreHide implementation complexity. Expose only what is necessary. Think in concepts, not mechanics.
Explore CurrentBuild on existing classes. Reuse code elegantly. Create natural type hierarchies.
You are hereOne interface, many forms. Duck typing. Write code that adapts to any conforming object.
ExploreSubscribe to Poetic Bytes
Level up your Python knowledge.
In-depth Python tutorials, AI insights, and software engineering guides — delivered straight to your inbox. No spam, ever.
🔒 Join 8,400+ developers. Your email is never shared or sold.
Follow Poetic Bytes
Stay connected across every platform. Get daily Python tips, OOP insights, and the latest AI developments — wherever you spend your time online.