Skip to main content

Python Inheritance — Single, Multiple, and: Practice Problems & Exercises

Practice: Inheritance — Single, Multiple, and Cooperative at Engineering Depth

11 problems4 Easy4 Medium3 Hard45-60 min
← Back to lesson

Easy

#1Predict the Output — Method Override and super()Easy
method overridesuper()output prediction

Predict all four outputs. Track which method is called for each object.

Python
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\nelectricity
Hints

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.


#2isinstance() and issubclass() — Predict the OutputEasy
isinstanceissubclasstype checkingoutput prediction

Predict all seven boolean outputs for the isinstance and issubclass calls.

Python
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\nFalse
Hints

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.


#3Extend a Built-in — Custom ListEasy
inheritanceextending builtinslist subclass

Subclass list and override append and extend to maintain sorted order. Override __init__ to sort on construction too.

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


#4super() in a Single-Inheritance ChainEasy
super()single inheritance__init__ chain

Complete the __init__ chain: Square delegates to Rectangle, which delegates to Shape, using super() at each level.

Python
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\n25
Hints

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

#5Multiple Inheritance — MRO TraceMedium
multiple inheritanceMROC3 linearisationoutput prediction

Trace the MRO and predict the output. The classic "diamond problem" with cooperative super() calls.

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


#6Mixin Classes — Add Serialisation to Any ClassMedium
mixinsmultiple inheritanceserialisation

Implement JSONMixin.to_json and JSONMixin.from_json. The mixin should work for any class whose __init__ parameters match its __dict__ keys.

Python
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\n30
Hints

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.


#7Override vs Extension — Template Method PatternMedium
template methodinheritanceabstract methods

Trace through the template method pattern. Predict the output of r.generate(), noting which methods are overridden vs inherited.

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


#8Cooperative super() in Multiple Inheritance __init__Medium
super()multiple inheritancecooperative __init__MRO

Trace the cooperative __init__ chain and predict the output. Each mixin consumes its own keyword argument from **kwargs.

Python
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

#9Abstract Base Class with Enforced InterfaceHard
ABCabstractmethodinterface enforcementpolymorphism

Read the ABC hierarchy and predict the output. Explain why Shape() raises TypeError and how describe() achieves polymorphism.

Python
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, perimeter
Hints

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.


#10Inheritance vs Composition — Refactor to CompositionHard
compositioninheritance vs compositionrefactoringdesign

Implement ComposedCar using composition rather than inheritance. A car has an engine, it is not an engine.

Python
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 stopped
Hints

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.


#11Plugin Architecture — Registry + ABC + FactoryHard
ABC__init_subclass__factoryplugin patternclass design

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.

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

© 2026 EngineersOfAI. All rights reserved.