Python Inheritance — Single, Multiple, and: Practice Problems & Exercises
Practice: Inheritance — Single, Multiple, and Cooperative at Engineering Depth
← Back to lessonEasy
Predict all four outputs. Track which method is called for each object.
class Vehicle:
def describe(self):
return "I am a vehicle"
def fuel(self):
return "gasoline"
class ElectricCar(Vehicle):
def fuel(self):
return "electricity"
def describe(self):
base = super().describe()
return f"{base} (electric)"
v = Vehicle()
e = ElectricCar()
print(v.describe())
print(e.describe())
print(v.fuel())
print(e.fuel())Solution
I am a vehicle
I am a vehicle (electric)
gasoline
electricity
Explanation: v.describe() and v.fuel() call the Vehicle methods directly. e.fuel() calls ElectricCar.fuel() which fully overrides Vehicle.fuel() — no base call, returns "electricity". e.describe() calls ElectricCar.describe(), which calls super().describe() to get "I am a vehicle" from Vehicle, then appends " (electric)". This is the extend-and-delegate pattern: you build on the parent's implementation rather than replacing it entirely.
class Vehicle:
def describe(self):
return "I am a vehicle"
def fuel(self):
return "gasoline"
class ElectricCar(Vehicle):
def fuel(self):
return "electricity"
def describe(self):
base = super().describe()
return f"{base} (electric)"
v = Vehicle()
e = ElectricCar()
print(v.describe())
print(e.describe())
print(v.fuel())
print(e.fuel())Expected Output
I am a vehicle\nI am a vehicle (electric)\ngasoline\nelectricityHints
Hint 1: ElectricCar.fuel() overrides Vehicle.fuel() completely — no super() call.
Hint 2: ElectricCar.describe() calls super().describe() to get the base string, then appends to it.
Hint 3: v.fuel() and v.describe() use Vehicle methods. e.fuel() and e.describe() use ElectricCar methods.
Predict all seven boolean outputs for the isinstance and issubclass calls.
class Animal:
pass
class Dog(Animal):
pass
class Cat(Animal):
pass
class GoldenRetriever(Dog):
pass
g = GoldenRetriever()
print(isinstance(g, GoldenRetriever))
print(isinstance(g, Dog))
print(isinstance(g, Animal))
print(isinstance(g, Cat))
print(issubclass(GoldenRetriever, Dog))
print(issubclass(GoldenRetriever, Animal))
print(issubclass(Dog, Cat))Solution
True
True
True
False
True
True
False
Explanation: isinstance walks the inheritance chain: g is a GoldenRetriever, which is a Dog, which is an Animal — so all three of those checks return True. g is not a Cat, so that check is False. issubclass checks class relationships: GoldenRetriever inherits from Dog (True) and transitively from Animal (True). Dog does not inherit from Cat (False). Both isinstance and issubclass accept tuples as the second argument: isinstance(g, (Dog, Cat)) returns True if g is an instance of either.
class Animal:
pass
class Dog(Animal):
pass
class Cat(Animal):
pass
class GoldenRetriever(Dog):
pass
g = GoldenRetriever()
print(isinstance(g, GoldenRetriever))
print(isinstance(g, Dog))
print(isinstance(g, Animal))
print(isinstance(g, Cat))
print(issubclass(GoldenRetriever, Dog))
print(issubclass(GoldenRetriever, Animal))
print(issubclass(Dog, Cat))Expected Output
True\nTrue\nTrue\nFalse\nTrue\nTrue\nFalseHints
Hint 1: isinstance(obj, cls) returns True if obj is an instance of cls or any subclass of cls.
Hint 2: issubclass(A, B) returns True if A is B or inherits from B (directly or indirectly).
Hint 3: GoldenRetriever is-a Dog, which is-a Animal — so isinstance(g, Animal) is True.
Subclass list and override append and extend to maintain sorted order. Override __init__ to sort on construction too.
class SortedList(list):
def __init__(self, iterable=None):
super().__init__(iterable or [])
self.sort()
def append(self, item):
super().append(item)
self.sort()
def extend(self, items):
super().extend(items)
self.sort()
sl = SortedList([3, 1, 4])
print(sl)
sl.append(2)
print(sl)
sl.extend([7, 0, 5])
print(sl)Solution
class SortedList(list):
def __init__(self, iterable=None):
super().__init__(iterable or [])
self.sort()
def append(self, item):
super().append(item)
self.sort()
def extend(self, items):
super().extend(items)
self.sort()
Explanation: Subclassing built-in types is fully supported. We call super().append(item) to use the C-implementation's fast append, then call self.sort() which is also the C-level list sort (Timsort, O(n log n)). Overriding __init__ to sort the initial iterable ensures the invariant holds from construction. Note: in a production setting, using bisect.insort instead of sort() would give O(n) insertion (binary search + shift) vs O(n log n) for full sort on each append.
class SortedList(list):
"""A list that stays sorted after every append."""
def append(self, item):
# TODO: append and then sort
pass
def extend(self, items):
# TODO: extend and then sort
pass
sl = SortedList([3, 1, 4])
print(sl)
sl.append(2)
print(sl)
sl.extend([7, 0, 5])
print(sl)Expected Output
[1, 3, 4]\n[1, 2, 3, 4]\n[0, 1, 2, 3, 4, 5, 7]Hints
Hint 1: Call super().append(item) to add the item, then self.sort() to keep order.
Hint 2: Call super().extend(items) to add all items, then self.sort().
Hint 3: The __init__ does not need overriding — list.__init__ accepts an iterable and sorts it on construction via self.sort() in a different way... actually just call sort in __init__ too.
Complete the __init__ chain: Square delegates to Rectangle, which delegates to Shape, using super() at each level.
class Shape:
def __init__(self, color):
self.color = color
def area(self):
return 0
class Rectangle(Shape):
def __init__(self, color, width, height):
super().__init__(color)
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Square(Rectangle):
def __init__(self, color, side):
super().__init__(color, side, side)
s = Square("red", 5)
print(s.color)
print(s.width)
print(s.area())Solution
class Rectangle(Shape):
def __init__(self, color, width, height):
super().__init__(color)
self.width = width
self.height = height
class Square(Rectangle):
def __init__(self, color, side):
super().__init__(color, side, side)
Explanation: The chain is Square.__init__ → Rectangle.__init__ → Shape.__init__. At each level, super().__init__ forwards to the next class in the MRO. Square passes side for both width and height to Rectangle.__init__, which sets both attributes and then calls Shape.__init__ to set color. After construction, s has all three attributes and inherits the area method from Rectangle. This is the standard cooperative constructor pattern for linear single-inheritance hierarchies.
class Shape:
def __init__(self, color):
self.color = color
def area(self):
return 0
class Rectangle(Shape):
def __init__(self, color, width, height):
# TODO: call Shape.__init__ with color
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Square(Rectangle):
def __init__(self, color, side):
# TODO: call Rectangle.__init__ with color, side, side
pass
s = Square("red", 5)
print(s.color)
print(s.width)
print(s.area())Expected Output
red\n5\n25Hints
Hint 1: In Rectangle.__init__, call super().__init__(color) to set self.color via Shape.
Hint 2: In Square.__init__, call super().__init__(color, side, side) to reuse Rectangle.__init__.
Hint 3: After Square.__init__ completes, s.color, s.width, s.height, and s.area() all work.
Medium
Trace the MRO and predict the output. The classic "diamond problem" with cooperative super() calls.
class A:
def hello(self):
return "A"
class B(A):
def hello(self):
return "B->" + super().hello()
class C(A):
def hello(self):
return "C->" + super().hello()
class D(B, C):
def hello(self):
return "D->" + super().hello()
d = D()
print(d.hello())
print(D.__mro__)Solution
D->B->C->A
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
Explanation: Python's C3 linearisation algorithm computes the MRO for D as [D, B, C, A, object]. When d.hello() runs: D.hello() calls super().hello(), which according to the MRO is B.hello(). B.hello() calls super().hello(), which in the context of D's MRO is C.hello() (not A.hello()). C.hello() calls super().hello(), which is A.hello(). Concatenating: "D->" + "B->" + "C->" + "A" = "D->B->C->A". super() always refers to the next class in the calling instance's MRO, making cooperative multiple inheritance possible.
class A:
def hello(self):
return "A"
class B(A):
def hello(self):
return "B->" + super().hello()
class C(A):
def hello(self):
return "C->" + super().hello()
class D(B, C):
def hello(self):
return "D->" + super().hello()
d = D()
print(d.hello())
print(D.__mro__)Expected Output
D->B->C->A\n(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)Hints
Hint 1: The MRO for D is computed by C3 linearisation: D, B, C, A, object.
Hint 2: super() in D.hello() finds B.hello(); super() in B.hello() finds C.hello() (not A!); super() in C.hello() finds A.hello().
Hint 3: Each super() advances to the next class in the MRO, not just the immediate parent.
Implement JSONMixin.to_json and JSONMixin.from_json. The mixin should work for any class whose __init__ parameters match its __dict__ keys.
import json
class JSONMixin:
def to_json(self):
return json.dumps(self.__dict__)
@classmethod
def from_json(cls, json_str):
data = json.loads(json_str)
return cls(**data)
class User(JSONMixin):
def __init__(self, name, email, age):
self.name = name
self.email = email
self.age = age
u = User("Alice", "[email protected]", 30)
j = u.to_json()
print(j)
u2 = User.from_json(j)
print(u2.name)
print(u2.age)Solution
class JSONMixin:
def to_json(self):
return json.dumps(self.__dict__)
@classmethod
def from_json(cls, json_str):
data = json.loads(json_str)
return cls(**data)
Explanation: Mixins add behaviour to any class without being a standalone base class. JSONMixin uses self.__dict__ — the instance attribute dictionary — to serialise state. from_json is a @classmethod that uses cls(**data) instead of a hardcoded class name, making it work correctly for User and any other class that inherits the mixin. The key assumption is that __dict__ keys match constructor parameter names. This breaks with __slots__ classes (no __dict__) or classes with non-serialisable attributes — for those, you would need a custom to_dict method.
import json
class JSONMixin:
def to_json(self):
# TODO: serialise self.__dict__ to a JSON string
pass
@classmethod
def from_json(cls, json_str):
# TODO: parse json_str and pass as kwargs to cls()
pass
class User(JSONMixin):
def __init__(self, name, email, age):
self.name = name
self.email = email
self.age = age
u = User("Alice", "[email protected]", 30)
j = u.to_json()
print(j)
u2 = User.from_json(j)
print(u2.name)
print(u2.age)Expected Output
{"name": "Alice", "email": "[email protected]", "age": 30}\nAlice\n30Hints
Hint 1: to_json should return json.dumps(self.__dict__).
Hint 2: from_json should parse with json.loads(json_str) and call cls(**data).
Hint 3: The mixin relies on the concrete class having a constructor that accepts keyword arguments matching __dict__ keys.
Trace through the template method pattern. Predict the output of r.generate(), noting which methods are overridden vs inherited.
class Report:
def generate(self):
lines = []
lines.append(self.header())
lines.extend(self.body())
lines.append(self.footer())
return "\n".join(lines)
def header(self):
return "=== Report ==="
def body(self):
raise NotImplementedError("Subclasses must implement body()")
def footer(self):
return "=== End ==="
class SalesReport(Report):
def __init__(self, sales):
self.sales = sales
def header(self):
return f"=== Sales Report (total: {sum(self.sales)}) ==="
def body(self):
return [f" - {s}" for s in self.sales]
r = SalesReport([100, 250, 75])
print(r.generate())Solution
=== Sales Report (total: 425) ===
- 100
- 250
- 75
=== End ===
Explanation: The Template Method pattern defines an algorithm's skeleton in the base class (generate) and lets subclasses override specific steps. SalesReport overrides header() (customised with total) and body() (line-by-line sales). It inherits footer() unchanged. generate() orchestrates all three — always calling the method on self, so the overridden versions are used where defined. Raising NotImplementedError in the base body() makes the contract explicit: any direct Report() instantiation that calls generate() fails with a clear message. For stricter enforcement, use abc.abstractmethod.
class Report:
"""Template method pattern: base class defines the skeleton."""
def generate(self):
lines = []
lines.append(self.header())
lines.extend(self.body())
lines.append(self.footer())
return "
".join(lines)
def header(self):
return "=== Report ==="
def body(self):
raise NotImplementedError("Subclasses must implement body()")
def footer(self):
return "=== End ==="
class SalesReport(Report):
def __init__(self, sales):
self.sales = sales
def header(self):
return f"=== Sales Report (total: {sum(self.sales)}) ==="
def body(self):
return [f" - {s}" for s in self.sales]
r = SalesReport([100, 250, 75])
print(r.generate())Expected Output
=== Sales Report (total: 425) ===\n - 100\n - 250\n - 75\n=== End ===Hints
Hint 1: SalesReport overrides header() but not footer() — footer uses the base implementation.
Hint 2: body() returns a list of strings; generate() extends the lines list with them.
Hint 3: The base generate() method defines the skeleton — subclasses customise individual steps.
Trace the cooperative __init__ chain and predict the output. Each mixin consumes its own keyword argument from **kwargs.
class Flyable:
def __init__(self, max_altitude, **kwargs):
super().__init__(**kwargs)
self.max_altitude = max_altitude
def fly(self):
return f"Flying up to {self.max_altitude}m"
class Swimmable:
def __init__(self, max_depth, **kwargs):
super().__init__(**kwargs)
self.max_depth = max_depth
def swim(self):
return f"Diving to {self.max_depth}m"
class Duck(Flyable, Swimmable):
def __init__(self, name, max_altitude, max_depth):
super().__init__(max_altitude=max_altitude, max_depth=max_depth)
self.name = name
d = Duck("Donald", max_altitude=100, max_depth=3)
print(d.name)
print(d.fly())
print(d.swim())
print(Duck.__mro__)Solution
Donald
Flying up to 100m
Diving to 3m
(<class '__main__.Duck'>, <class '__main__.Flyable'>, <class '__main__.Swimmable'>, <class 'object'>)
Explanation: The MRO for Duck is [Duck, Flyable, Swimmable, object]. Duck.__init__ calls super().__init__(max_altitude=100, max_depth=3). This reaches Flyable.__init__, which takes max_altitude=100 and passes max_depth=3 along via **kwargs to super().__init__(**kwargs). That reaches Swimmable.__init__, which takes max_depth=3 and passes an empty **kwargs to super().__init__() — which is object.__init__(). Each mixin sets its own attribute after the super call returns. After the chain, d has max_altitude, max_depth, and name. The **kwargs forwarding pattern is essential for cooperative multiple-inheritance constructors.
class Flyable:
def __init__(self, max_altitude, **kwargs):
super().__init__(**kwargs)
self.max_altitude = max_altitude
def fly(self):
return f"Flying up to {self.max_altitude}m"
class Swimmable:
def __init__(self, max_depth, **kwargs):
super().__init__(**kwargs)
self.max_depth = max_depth
def swim(self):
return f"Diving to {self.max_depth}m"
class Duck(Flyable, Swimmable):
def __init__(self, name, max_altitude, max_depth):
super().__init__(max_altitude=max_altitude, max_depth=max_depth)
self.name = name
d = Duck("Donald", max_altitude=100, max_depth=3)
print(d.name)
print(d.fly())
print(d.swim())
print(Duck.__mro__)Expected Output
Donald\nFlying up to 100m\nDiving to 3m\n(<class '__main__.Duck'>, <class '__main__.Flyable'>, <class '__main__.Swimmable'>, <class 'object'>)Hints
Hint 1: Each mixin passes **kwargs through via super().__init__(**kwargs), consuming its own keyword argument.
Hint 2: MRO is Duck → Flyable → Swimmable → object.
Hint 3: Flyable.__init__ consumes max_altitude; Swimmable.__init__ consumes max_depth; object.__init__ gets an empty kwargs.
Hard
Read the ABC hierarchy and predict the output. Explain why Shape() raises TypeError and how describe() achieves polymorphism.
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
def describe(self):
return f"{type(self).__name__}: area={self.area():.2f}, perimeter={self.perimeter():.2f}"
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
class Triangle(Shape):
def __init__(self, a, b, c):
self.a, self.b, self.c = a, b, c
def area(self):
s = (self.a + self.b + self.c) / 2
return math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))
def perimeter(self):
return self.a + self.b + self.c
shapes = [Circle(5), Triangle(3, 4, 5)]
for s in shapes:
print(s.describe())
try:
bad = Shape()
except TypeError as e:
print(f"TypeError: {e}")Solution
Circle: area=78.54, perimeter=31.42
Triangle: area=6.00, perimeter=12
TypeError: Can't instantiate abstract class Shape with abstract methods area, perimeter
Explanation: ABC uses ABCMeta as its metaclass. ABCMeta.__call__ checks whether all @abstractmethod methods are overridden before allowing instantiation — if any remain abstract, it raises TypeError. Circle and Triangle both implement area and perimeter, so they are concrete and can be instantiated. The describe method in Shape demonstrates polymorphism: self.area() and self.perimeter() dispatch to the concrete subclass implementations at runtime. For the 3-4-5 right triangle, Heron's formula gives area 6.00 exactly. The :.2f format spec in describe rounds to 2 decimal places.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
def describe(self):
return f"{type(self).__name__}: area={self.area():.2f}, perimeter={self.perimeter():.2f}"
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
def perimeter(self):
import math
return 2 * math.pi * self.radius
class Triangle(Shape):
def __init__(self, a, b, c):
self.a, self.b, self.c = a, b, c
def area(self):
import math
s = (self.a + self.b + self.c) / 2
return math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))
def perimeter(self):
return self.a + self.b + self.c
shapes = [Circle(5), Triangle(3, 4, 5)]
for s in shapes:
print(s.describe())
try:
bad = Shape()
except TypeError as e:
print(f"TypeError: {e}")Expected Output
Circle: area=78.54, perimeter=31.42\nTriangle: area=6.00, perimeter=12\nTypeError: Can't instantiate abstract class Shape with abstract methods area, perimeterHints
Hint 1: Shape cannot be instantiated because it has abstract methods — this is enforced by ABCMeta.
Hint 2: Circle and Triangle provide concrete implementations of both abstract methods.
Hint 3: describe() in the base class calls self.area() and self.perimeter() — polymorphism at work.
Implement ComposedCar using composition rather than inheritance. A car has an engine, it is not an engine.
class Engine:
def start(self):
return "engine started"
def stop(self):
return "engine stopped"
def status(self):
return "running"
class ComposedCar:
def __init__(self):
self.engine = Engine()
def drive(self):
status = self.engine.start()
return f"driving ({status})"
def park(self):
return self.engine.stop()
cc = ComposedCar()
print(cc.drive())
print(cc.park())Solution
class ComposedCar:
def __init__(self):
self.engine = Engine()
def drive(self):
status = self.engine.start()
return f"driving ({status})"
def park(self):
return self.engine.stop()
Explanation: The inheritance version (Car(Engine)) is wrong semantically — a car is not a kind of engine. It also exposes all Engine methods publicly on Car, which leaks implementation details. The composition version stores an Engine as a component and delegates to it explicitly. This is the "favour composition over inheritance" principle. Benefits: ComposedCar can swap engine types at runtime (self.engine = ElectricEngine()), only the methods it explicitly delegates are public, and changing Engine's interface does not automatically break ComposedCar. Use inheritance only when the subclass truly is-a base class.
# INHERITANCE VERSION (fragile base class problem)
class Engine:
def start(self):
return "engine started"
def stop(self):
return "engine stopped"
def status(self):
return "running"
class Car(Engine):
"""Inherits Engine — but a Car IS NOT an engine."""
def drive(self):
return f"driving ({self.status()})"
# COMPOSITION VERSION — implement this
class ComposedCar:
def __init__(self):
# TODO: create an Engine instance as self.engine
pass
def drive(self):
# TODO: start engine and return drive message
pass
def park(self):
# TODO: stop engine
pass
cc = ComposedCar()
print(cc.drive())
print(cc.park())Expected Output
driving (engine started)\nengine stoppedHints
Hint 1: Store self.engine = Engine() in __init__.
Hint 2: In drive(), call self.engine.start() and use its return value in the message.
Hint 3: In park(), call self.engine.stop() and return its value.
Read the complete plugin architecture and predict the exact output. Then explain how __init_subclass__, ABC, and a factory method combine to create an extensible plugin system.
from abc import ABC, abstractmethod
import json
class Exporter(ABC):
_registry = {}
def __init_subclass__(cls, fmt=None, **kwargs):
super().__init_subclass__(**kwargs)
if fmt:
Exporter._registry[fmt] = cls
@abstractmethod
def export(self, data):
pass
@classmethod
def create(cls, fmt):
if fmt not in cls._registry:
raise ValueError(f"Unknown format: {fmt}")
return cls._registry[fmt]()
class CSVExporter(Exporter, fmt="csv"):
def export(self, data):
header = ",".join(data[0].keys())
rows = [",".join(str(v) for v in row.values()) for row in data]
return "\n".join([header] + rows)
class JSONExporter(Exporter, fmt="json"):
def export(self, data):
return json.dumps(data)
records = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
csv_exp = Exporter.create("csv")
print(csv_exp.export(records))
print("---")
json_exp = Exporter.create("json")
print(json_exp.export(records))Solution
name,age
Alice,30
Bob,25
---
[{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
Explanation: Three mechanisms work together. ABC with @abstractmethod enforces that every Exporter subclass implements export — you cannot instantiate an incomplete plugin. __init_subclass__ is called when each subclass is defined; it extracts the fmt keyword and registers the class. Exporter.create("csv") is a factory method that performs a registry lookup and instantiates the correct class — callers never import CSVExporter directly. This architecture is open for extension (add new exporters without modifying Exporter) and closed for modification — the Open/Closed Principle. Adding a XMLExporter(Exporter, fmt="xml") anywhere in the codebase would be automatically available via Exporter.create("xml").
from abc import ABC, abstractmethod
class Exporter(ABC):
_registry = {}
def __init_subclass__(cls, fmt=None, **kwargs):
super().__init_subclass__(**kwargs)
if fmt:
Exporter._registry[fmt] = cls
@abstractmethod
def export(self, data):
pass
@classmethod
def create(cls, fmt):
if fmt not in cls._registry:
raise ValueError(f"Unknown format: {fmt}")
return cls._registry[fmt]()
class CSVExporter(Exporter, fmt="csv"):
def export(self, data):
header = ",".join(data[0].keys())
rows = [",".join(str(v) for v in row.values()) for row in data]
return "
".join([header] + rows)
class JSONExporter(Exporter, fmt="json"):
def export(self, data):
import json
return json.dumps(data)
records = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
csv_exp = Exporter.create("csv")
print(csv_exp.export(records))
print("---")
json_exp = Exporter.create("json")
print(json_exp.export(records))Expected Output
name,age\nAlice,30\nBob,25\n---\n[{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]Hints
Hint 1: __init_subclass__ registers each subclass under its fmt keyword when the class is defined.
Hint 2: Exporter.create("csv") looks up CSVExporter in the registry and instantiates it.
Hint 3: CSVExporter cannot be instantiated without implementing export() — enforced by ABC.
