Skip to main content

Python Abstract Base Classes — Enforcing: Practice Problems & Exercises

Practice: Abstract Base Classes — Enforcing Interfaces at Engineering Depth

11 problems4 Easy4 Medium3 Hard40-55 min
← Back to lesson

Easy

#1Define a Simple ABCEasy
abcabstractmethod

Implement Circle.area() to satisfy the Shape ABC. The area of a circle is pi * r squared.

Python
import math
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        pass

class Circle(Shape):
    def __init__(self, radius: float) -> None:
        self.radius = radius

    def area(self) -> float:
        return math.pi * self.radius ** 2

c = Circle(5)
print(c.area())
Solution
import math
from abc import ABC, abstractmethod

class Shape(ABC):
@abstractmethod
def area(self) -> float:
"""Return the area of the shape."""

class Circle(Shape):
def __init__(self, radius: float) -> None:
self.radius = radius

def area(self) -> float:
return math.pi * self.radius ** 2

class Rectangle(Shape):
def __init__(self, width: float, height: float) -> None:
self.width = width
self.height = height

def area(self) -> float:
return self.width * self.height

c = Circle(5)
print(c.area()) # 78.53981633974483

r = Rectangle(4, 6)
print(r.area()) # 24.0

# Cannot instantiate Shape directly:
try:
Shape()
except TypeError as e:
print(e) # Can't instantiate abstract class Shape...

Explanation: @abstractmethod converts area into a contract. Any concrete subclass that fails to implement it will raise TypeError when you try to instantiate it. This enforces the interface at runtime, making the error explicit and early.

from abc import ABC, abstractmethod

class Shape(ABC):
  @abstractmethod
  def area(self) -> float:
      pass

class Circle(Shape):
  def __init__(self, radius: float):
      self.radius = radius

  # TODO: implement area()
  pass

c = Circle(5)
print(c.area())
Expected Output
78.53981633974483
Hints

Hint 1: Use import math and return math.pi * self.radius ** 2.

Hint 2: Forgetting to implement area() would raise TypeError on instantiation.


#2Prevent Instantiation of an ABCEasy
abcTypeErrorinstantiation

Verify that Python prevents direct instantiation of an ABC by catching the TypeError it raises.

Python
from abc import ABC, abstractmethod

class Transport(ABC):
    @abstractmethod
    def move(self) -> str:
        pass

try:
    t = Transport()
except TypeError as e:
    print(f"Caught: {e}")
Solution
from abc import ABC, abstractmethod

class Transport(ABC):
@abstractmethod
def move(self) -> str:
pass

try:
t = Transport()
except TypeError as e:
print(f"Caught: {e}")
# Caught: Can't instantiate abstract class Transport without
# an implementation for abstract method 'move'

# A concrete subclass CAN be instantiated:
class Car(Transport):
def move(self) -> str:
return "driving"

car = Car()
print(car.move()) # driving

Explanation: ABC uses ABCMeta as its metaclass. The metaclass's __call__ checks whether all abstract methods are implemented before allowing instantiation. This gives you compile-time–like enforcement in a dynamic language — the error surfaces the moment you try to create an object, not when you call the missing method.

from abc import ABC, abstractmethod

class Transport(ABC):
  @abstractmethod
  def move(self) -> str:
      pass

# TODO: try to instantiate Transport and catch the TypeError
try:
  t = Transport()
except TypeError as e:
  print(f"Caught: {e}")
Expected Output
Caught: Can't instantiate abstract class Transport without an implementation for abstract method 'move'
Hints

Hint 1: Wrap Transport() in a try/except TypeError block.

Hint 2: The error message names the missing abstract method.


#3Multiple Abstract MethodsEasy
abcabstractmethodinterface

Implement JSONSerializer by providing concrete implementations for both abstract methods defined in Serializer.

Python
import json
from abc import ABC, abstractmethod

class Serializer(ABC):
    @abstractmethod
    def serialize(self, obj) -> str:
        pass

    @abstractmethod
    def deserialize(self, data: str):
        pass

class JSONSerializer(Serializer):
    def serialize(self, obj) -> str:
        return json.dumps(obj)

    def deserialize(self, data: str):
        return json.loads(data)

s = JSONSerializer()
data = s.serialize({"key": "value"})
print(data)
print(s.deserialize(data))
Solution
import json
from abc import ABC, abstractmethod

class Serializer(ABC):
@abstractmethod
def serialize(self, obj) -> str:
"""Convert obj to a string representation."""

@abstractmethod
def deserialize(self, data: str):
"""Parse a string and return the Python object."""

class JSONSerializer(Serializer):
def serialize(self, obj) -> str:
return json.dumps(obj)

def deserialize(self, data: str):
return json.loads(data)

class ReprSerializer(Serializer):
def serialize(self, obj) -> str:
return repr(obj)

def deserialize(self, data: str):
return eval(data) # simplified; never eval untrusted input

s = JSONSerializer()
data = s.serialize({"key": "value"})
print(data) # {"key": "value"}
print(s.deserialize(data)) # {'key': 'value'}

# Verify: partial implementation still raises TypeError
try:
class BadSerializer(Serializer):
def serialize(self, obj) -> str:
return str(obj)
BadSerializer()
except TypeError as e:
print(e)

Explanation: All abstract methods must be implemented for a concrete subclass to be instantiable. If even one is missing, Python raises TypeError. This ensures the interface contract is complete — callers can rely on every method being present.

from abc import ABC, abstractmethod

class Serializer(ABC):
  @abstractmethod
  def serialize(self, obj) -> str:
      pass

  @abstractmethod
  def deserialize(self, data: str):
      pass

import json

class JSONSerializer(Serializer):
  # TODO: implement both methods
  pass

s = JSONSerializer()
data = s.serialize({"key": "value"})
print(data)
print(s.deserialize(data))
Expected Output
{"key": "value"}
{'key': 'value'}
Hints

Hint 1: serialize() should return json.dumps(obj).

Hint 2: deserialize() should return json.loads(data).


#4isinstance Check with ABCEasy
abcisinstanceissubclass

Verify that isinstance and issubclass work correctly with ABC hierarchies. Both Dog and Cat should be recognised as Animal instances.

Python
from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self) -> str:
        pass

class Dog(Animal):
    def speak(self) -> str:
        return "Woof"

class Cat(Animal):
    def speak(self) -> str:
        return "Meow"

dog, cat = Dog(), Cat()
print(isinstance(dog, Animal))    # True
print(isinstance(cat, Animal))    # True
print(issubclass(Dog, Animal))    # True
Solution
from abc import ABC, abstractmethod

class Animal(ABC):
@abstractmethod
def speak(self) -> str:
pass

class Dog(Animal):
def speak(self) -> str:
return "Woof"

class Cat(Animal):
def speak(self) -> str:
return "Meow"

animals = [Dog(), Cat()]
for animal in animals:
print(isinstance(animal, Animal), animal.speak())
# True Woof
# True Meow

print(issubclass(Dog, Animal)) # True
print(issubclass(Cat, Animal)) # True
print(issubclass(Animal, Animal)) # True (a class is a subclass of itself)

Explanation: isinstance checks the instance's class against the ABC and returns True if the class is a subclass. This enables writing functions that accept Animal as a type annotation and checking at runtime with isinstance, making ABCs useful both for static type checking and runtime dispatch.

from abc import ABC, abstractmethod

class Animal(ABC):
  @abstractmethod
  def speak(self) -> str:
      pass

class Dog(Animal):
  def speak(self) -> str:
      return "Woof"

class Cat(Animal):
  def speak(self) -> str:
      return "Meow"

dog = Dog()
cat = Cat()

# TODO: print isinstance results for dog and cat against Animal
print(isinstance(dog, Animal))
print(isinstance(cat, Animal))
print(issubclass(Dog, Animal))
Expected Output
True\nTrue\nTrue
Hints

Hint 1: isinstance(obj, ABC) returns True for any concrete subclass instance.

Hint 2: issubclass(ConcreteClass, ABC) returns True for any registered or inheriting subclass.


Medium

#5Abstract PropertyMedium
abcabstractmethodproperty

Implement DevConfig to satisfy the Config ABC by providing concrete @property implementations for both abstract properties.

Python
from abc import ABC, abstractmethod

class Config(ABC):
    @property
    @abstractmethod
    def database_url(self) -> str:
        pass

    @property
    @abstractmethod
    def debug(self) -> bool:
        pass

class DevConfig(Config):
    @property
    def database_url(self) -> str:
        return "sqlite:///dev.db"

    @property
    def debug(self) -> bool:
        return True

cfg = DevConfig()
print(cfg.database_url)
print(cfg.debug)
Solution
from abc import ABC, abstractmethod

class Config(ABC):
@property
@abstractmethod
def database_url(self) -> str:
"""Database connection URL."""

@property
@abstractmethod
def debug(self) -> bool:
"""Whether debug mode is enabled."""

class DevConfig(Config):
@property
def database_url(self) -> str:
return "sqlite:///dev.db"

@property
def debug(self) -> bool:
return True

class ProdConfig(Config):
@property
def database_url(self) -> str:
return "postgresql://prod-host/mydb"

@property
def debug(self) -> bool:
return False

dev = DevConfig()
prod = ProdConfig()
print(dev.database_url) # sqlite:///dev.db
print(dev.debug) # True
print(prod.database_url) # postgresql://prod-host/mydb
print(prod.debug) # False

Explanation: Combining @property and @abstractmethod (always stack them with @property on the outside) enforces that subclasses expose values via property access. This prevents subclasses from accidentally making database_url a plain attribute while maintaining the ABC contract.

from abc import ABC, abstractmethod

class Config(ABC):
  @property
  @abstractmethod
  def database_url(self) -> str:
      pass

  @property
  @abstractmethod
  def debug(self) -> bool:
      pass

class DevConfig(Config):
  # TODO: implement database_url and debug as properties
  pass

cfg = DevConfig()
print(cfg.database_url)
print(cfg.debug)
Expected Output
sqlite:///dev.db\nTrue
Hints

Hint 1: Use @property on the concrete method (no need to repeat @abstractmethod).

Hint 2: Return a hardcoded string for database_url and True for debug.


#6Virtual Subclass via register()Medium
abcregistervirtual-subclass

Register LegacyWidget as a virtual subclass of Drawable using register(). The widget should then pass isinstance checks without inheriting from Drawable.

Python
from abc import ABC, abstractmethod

class Drawable(ABC):
    @abstractmethod
    def draw(self) -> str:
        pass

class LegacyWidget:
    def draw(self) -> str:
        return "drawing legacy widget"

Drawable.register(LegacyWidget)

widget = LegacyWidget()
print(isinstance(widget, Drawable))   # True
print(widget.draw())
Solution
from abc import ABC, abstractmethod

class Drawable(ABC):
@abstractmethod
def draw(self) -> str:
pass

# Third-party or legacy class — cannot modify its source
class LegacyWidget:
def draw(self) -> str:
return "drawing legacy widget"

# Register: tells Python that LegacyWidget fulfils the Drawable interface
Drawable.register(LegacyWidget)

widget = LegacyWidget()
print(isinstance(widget, Drawable)) # True
print(issubclass(LegacyWidget, Drawable)) # True
print(widget.draw()) # drawing legacy widget

# IMPORTANT: register() does NOT enforce the interface
class BadWidget:
pass # no draw() method

Drawable.register(BadWidget)
bad = BadWidget()
print(isinstance(bad, Drawable)) # True — but draw() would fail at runtime

Explanation: register() is for retrofitting existing classes (e.g., third-party or legacy code you cannot modify) into an ABC hierarchy. It makes isinstance/issubclass return True but does NOT verify that the registered class actually implements the abstract methods — that is the critical trade-off compared to real inheritance.

from abc import ABC, abstractmethod

class Drawable(ABC):
  @abstractmethod
  def draw(self) -> str:
      pass

# This class does NOT inherit from Drawable
class LegacyWidget:
  def draw(self) -> str:
      return "drawing legacy widget"

# TODO: register LegacyWidget as a virtual subclass of Drawable
# Then verify isinstance returns True

widget = LegacyWidget()
print(isinstance(widget, Drawable))
print(widget.draw())
Expected Output
True\ndrawing legacy widget
Hints

Hint 1: Call Drawable.register(LegacyWidget) after both classes are defined.

Hint 2: Virtual registration does NOT enforce that draw() is implemented — that is one difference from real subclassing.


#7Use collections.abc.SequenceMedium
collections.abcSequenceabc

Implement the two required abstract methods (__getitem__ and __len__) of collections.abc.Sequence. The mixin methods (__contains__, __iter__) are provided for free.

Python
from collections.abc import Sequence

class FixedList(Sequence):
    def __init__(self, *items):
        self._items = list(items)

    def __getitem__(self, index):
        return self._items[index]

    def __len__(self):
        return len(self._items)

fl = FixedList(10, 20, 30)
print(len(fl))
print(fl[1])
print(list(fl))
print(10 in fl)
Solution
from collections.abc import Sequence

class FixedList(Sequence):
def __init__(self, *items) -> None:
self._items = list(items)

def __getitem__(self, index):
return self._items[index]

def __len__(self) -> int:
return len(self._items)

fl = FixedList(10, 20, 30, 40, 50)

print(len(fl)) # 5
print(fl[1]) # 20
print(list(fl)) # [10, 20, 30, 40, 50]
print(10 in fl) # True
print(fl.index(30)) # 2 — provided by Sequence mixin
print(fl.count(10)) # 1 — provided by Sequence mixin

# isinstance works:
print(isinstance(fl, Sequence)) # True

Explanation: collections.abc.Sequence is a real ABC with mixin methods. Implement __getitem__ and __len__ and you get __contains__, __iter__, __reversed__, index, and count at no cost. This is the power of ABCs as reusable interface contracts with default behaviour.

from collections.abc import Sequence

class FixedList(Sequence):
  def __init__(self, *items):
      self._items = list(items)

  def __getitem__(self, index):
      # TODO: implement
      pass

  def __len__(self):
      # TODO: implement
      pass

fl = FixedList(10, 20, 30)
print(len(fl))
print(fl[1])
print(list(fl))
print(10 in fl)
Expected Output
3\n20\n[10, 20, 30]\nTrue
Hints

Hint 1: __getitem__ should return self._items[index].

Hint 2: __len__ should return len(self._items).

Hint 3: Sequence provides __contains__, __iter__, __reversed__, index(), and count() for free once you implement the two abstract methods.


#8ABC vs Protocol — Side-by-SideMedium
abctyping.Protocolcomparison

Demonstrate the key difference between ABCs and Protocols: ABCs require explicit inheritance; Protocols use structural (duck) typing. Both launch_abc and launch_protocol should work correctly.

Python
from abc import ABC, abstractmethod
from typing import Protocol

class Flyable(ABC):
    @abstractmethod
    def fly(self) -> str:
        pass

class Eagle(Flyable):
    def fly(self) -> str:
        return "Eagle flying"

class CanFly(Protocol):
    def fly(self) -> str: ...

class Drone:
    def fly(self) -> str:
        return "Drone flying"

def launch_abc(thing: Flyable) -> str:
    return thing.fly()

def launch_protocol(thing: CanFly) -> str:
    return thing.fly()

print(launch_abc(Eagle()))
print(launch_protocol(Drone()))
Solution
from abc import ABC, abstractmethod
from typing import Protocol, runtime_checkable

# --- ABC: nominal (inheritance-based) ---
class Flyable(ABC):
@abstractmethod
def fly(self) -> str:
pass

class Eagle(Flyable):
def fly(self) -> str:
return "Eagle flying"

# --- Protocol: structural (duck-typing-based) ---
@runtime_checkable
class CanFly(Protocol):
def fly(self) -> str: ...

class Drone: # no explicit parent needed
def fly(self) -> str:
return "Drone flying"

def launch_abc(thing: Flyable) -> str:
return thing.fly()

def launch_protocol(thing: CanFly) -> str:
return thing.fly()

print(launch_abc(Eagle())) # Eagle flying
print(launch_protocol(Drone())) # Drone flying

# With @runtime_checkable Protocol:
print(isinstance(Drone(), CanFly)) # True
print(isinstance(Eagle(), CanFly)) # True (Eagle also has fly())

# ABC isinstance:
print(isinstance(Drone(), Flyable)) # False — Drone doesn't inherit Flyable

Explanation: ABCs enforce the interface through the class hierarchy and enable register() for retrofitting. Protocols are purely structural — any object with the matching method signature satisfies the interface. Use ABCs when you control the hierarchy and want enforcement; use Protocols for open-world scenarios or third-party classes.

from abc import ABC, abstractmethod
from typing import Protocol

# ABC approach
class Flyable(ABC):
  @abstractmethod
  def fly(self) -> str:
      pass

class Eagle(Flyable):
  def fly(self) -> str:
      return "Eagle flying"

# Protocol approach
class CanFly(Protocol):
  def fly(self) -> str: ...

class Drone:  # does NOT inherit anything
  def fly(self) -> str:
      return "Drone flying"

def launch_abc(thing: Flyable) -> str:
  return thing.fly()

def launch_protocol(thing: CanFly) -> str:
  return thing.fly()

# TODO: call both launch functions and print results
print(launch_abc(Eagle()))
print(launch_protocol(Drone()))
Expected Output
Eagle flying\nDrone flying
Hints

Hint 1: ABC requires explicit inheritance; Protocol uses structural (duck) typing.

Hint 2: Drone does not inherit CanFly but satisfies it because it has a matching fly() method.


Hard

#9Implement a Custom Mapping ABCHard
collections.abcMutableMappingabc

Implement CaseInsensitiveDict using collections.abc.MutableMapping. Keys should be normalised to lowercase internally so d["Name"] and d["name"] refer to the same entry.

Python
from collections.abc import MutableMapping

class CaseInsensitiveDict(MutableMapping):
    def __init__(self):
        self._store = {}

    def __setitem__(self, key, value):
        self._store[key.lower()] = value

    def __getitem__(self, key):
        return self._store[key.lower()]

    def __delitem__(self, key):
        del self._store[key.lower()]

    def __iter__(self):
        return iter(self._store)

    def __len__(self):
        return len(self._store)

d = CaseInsensitiveDict()
d["Name"] = "Alice"
print(d["name"])
print(d["NAME"])
print(len(d))
Solution
from collections.abc import MutableMapping

class CaseInsensitiveDict(MutableMapping):
def __init__(self, data=None) -> None:
self._store: dict = {}
if data:
self.update(data) # MutableMapping provides update()

def __setitem__(self, key: str, value) -> None:
self._store[key.lower()] = value

def __getitem__(self, key: str):
return self._store[key.lower()]

def __delitem__(self, key: str) -> None:
del self._store[key.lower()]

def __iter__(self):
return iter(self._store)

def __len__(self) -> int:
return len(self._store)

def __repr__(self) -> str:
return f"{self.__class__.__name__}({self._store!r})"

d = CaseInsensitiveDict({"Content-Type": "application/json"})
d["Authorization"] = "Bearer token"

print(d["content-type"]) # application/json
print(d["AUTHORIZATION"]) # Bearer token
print(len(d)) # 2
print(list(d.keys())) # ['content-type', 'authorization']
print("content-type" in d) # True

Explanation: MutableMapping requires __setitem__, __getitem__, __delitem__, __iter__, and __len__. In return it provides keys(), values(), items(), get(), pop(), popitem(), clear(), update(), and setdefault(). This is the ABC + mixin pattern at its most productive — implement five methods, get 10+ for free.

from collections.abc import MutableMapping

class CaseInsensitiveDict(MutableMapping):
  def __init__(self):
      self._store = {}

  def __setitem__(self, key, value):
      # TODO: store with lowercased key
      pass

  def __getitem__(self, key):
      # TODO: retrieve by lowercased key
      pass

  def __delitem__(self, key):
      # TODO: delete by lowercased key
      pass

  def __iter__(self):
      return iter(self._store)

  def __len__(self):
      return len(self._store)

d = CaseInsensitiveDict()
d["Name"] = "Alice"
print(d["name"])
print(d["NAME"])
print(len(d))
Expected Output
Alice\nAlice\n1
Hints

Hint 1: In __setitem__ and __getitem__, call key.lower() before using key in self._store.

Hint 2: MutableMapping provides keys(), values(), items(), get(), pop(), update(), setdefault() for free.


#10ABCMeta with __subclasshook__Hard
ABCMeta__subclasshook__virtual-subclass

Implement __subclasshook__ so that any class with a to_string method is automatically considered a subclass of Printable, without needing to call register().

Python
from abc import ABCMeta, abstractmethod

class Printable(metaclass=ABCMeta):
    @abstractmethod
    def to_string(self) -> str:
        pass

    @classmethod
    def __subclasshook__(cls, subclass):
        if cls is Printable:
            if any("to_string" in B.__dict__ for B in subclass.__mro__):
                return True
        return NotImplemented

class Report:
    def to_string(self) -> str:
        return "Report content"

class EmptyClass:
    pass

print(isinstance(Report(), Printable))       # True
print(isinstance(EmptyClass(), Printable))   # False
Solution
from abc import ABCMeta, abstractmethod

class Printable(metaclass=ABCMeta):
@abstractmethod
def to_string(self) -> str:
pass

@classmethod
def __subclasshook__(cls, subclass):
# Only customise for Printable itself, not its subclasses
if cls is Printable:
# Walk the entire MRO to find to_string
if any("to_string" in B.__dict__ for B in subclass.__mro__):
return True
# Let normal ABC machinery decide
return NotImplemented

class Report:
def to_string(self) -> str:
return "Report content"

class Summary(Report):
pass # inherits to_string via MRO

class EmptyClass:
pass

print(isinstance(Report(), Printable)) # True (direct)
print(isinstance(Summary(), Printable)) # True (inherited)
print(isinstance(EmptyClass(), Printable)) # False

# How collections.abc uses this pattern:
from collections.abc import Sized
class MyContainer:
def __len__(self): return 0

print(isinstance(MyContainer(), Sized)) # True — __subclasshook__ found __len__

Explanation: __subclasshook__ is the mechanism that powers collections.abc — it checks for the presence of dunder methods and makes any class with __len__ a virtual Sized, any class with __iter__ a virtual Iterable, etc. Return True to accept, False to reject unconditionally, or NotImplemented to defer to the normal inheritance check.

from abc import ABCMeta, abstractmethod

class Printable(metaclass=ABCMeta):
  @abstractmethod
  def to_string(self) -> str:
      pass

  @classmethod
  def __subclasshook__(cls, subclass):
      # TODO: return True if subclass has a 'to_string' method,
      # NotImplemented otherwise
      pass

class Report:
  def to_string(self) -> str:
      return "Report content"

class EmptyClass:
  pass

print(isinstance(Report(), Printable))
print(isinstance(EmptyClass(), Printable))
Expected Output
True\nFalse
Hints

Hint 1: Check if "to_string" is in subclass.__dict__ or any of its bases using hasattr.

Hint 2: Return True if the method exists, NotImplemented if not (NOT False — NotImplemented lets the normal ABC check proceed).


#11Plugin Architecture with ABC RegistryHard
abcdesign-patternsregistryabstractmethod

Implement a self-registering plugin architecture using __init_subclass__. Create JSONExporter and CSVExporter that register themselves automatically when defined.

Python
import json
from abc import ABC, abstractmethod
from typing import Dict, Type

class Exporter(ABC):
    _registry: Dict[str, Type["Exporter"]] = {}

    def __init_subclass__(cls, format_name: str = "", **kwargs):
        super().__init_subclass__(**kwargs)
        if format_name:
            Exporter._registry[format_name] = cls

    @abstractmethod
    def export(self, data: dict) -> str:
        pass

    @classmethod
    def get(cls, format_name: str) -> "Exporter":
        if format_name not in cls._registry:
            raise KeyError(f"Unknown format: {format_name}")
        return cls._registry[format_name]()

class JSONExporter(Exporter, format_name="json"):
    def export(self, data: dict) -> str:
        return json.dumps(data)

class CSVExporter(Exporter, format_name="csv"):
    def export(self, data: dict) -> str:
        keys = list(data.keys())
        values = [str(data[k]) for k in keys]
        return ",".join(keys) + "\n" + ",".join(values)

exporter = Exporter.get("json")
print(exporter.export({"a": 1}))

exporter2 = Exporter.get("csv")
print(exporter2.export({"a": 1, "b": 2}))
Solution
import json
from abc import ABC, abstractmethod
from typing import Dict, Type

class Exporter(ABC):
_registry: Dict[str, Type["Exporter"]] = {}

def __init_subclass__(cls, format_name: str = "", **kwargs) -> None:
super().__init_subclass__(**kwargs)
if format_name:
Exporter._registry[format_name] = cls
print(f"Registered exporter: '{format_name}' -> {cls.__name__}")

@abstractmethod
def export(self, data: dict) -> str:
pass

@classmethod
def get(cls, format_name: str) -> "Exporter":
if format_name not in cls._registry:
raise KeyError(f"No exporter registered for '{format_name}'")
return cls._registry[format_name]()

@classmethod
def available_formats(cls) -> list:
return list(cls._registry.keys())

class JSONExporter(Exporter, format_name="json"):
def export(self, data: dict) -> str:
return json.dumps(data)

class CSVExporter(Exporter, format_name="csv"):
def export(self, data: dict) -> str:
keys = list(data.keys())
values = [str(data[k]) for k in keys]
return ",".join(keys) + "\n" + ",".join(values)

print("Available:", Exporter.available_formats())

json_exp = Exporter.get("json")
print(json_exp.export({"a": 1, "b": 2})) # {"a": 1, "b": 2}

csv_exp = Exporter.get("csv")
print(csv_exp.export({"x": 10, "y": 20})) # x,y\n10,20

try:
Exporter.get("xml")
except KeyError as e:
print(e)

Explanation: __init_subclass__ is called on the base class every time a new subclass is created. By passing format_name as a keyword argument in the class definition, each exporter registers itself without any explicit register() call. This is a clean plugin pattern: drop a new exporter module into the codebase and it self-registers on import. The ABC contract ensures every registered exporter implements export().

from abc import ABC, abstractmethod
from typing import Dict, Type

class Exporter(ABC):
  _registry: Dict[str, Type["Exporter"]] = {}

  def __init_subclass__(cls, format_name: str = "", **kwargs):
      super().__init_subclass__(**kwargs)
      if format_name:
          Exporter._registry[format_name] = cls

  @abstractmethod
  def export(self, data: dict) -> str:
      pass

  @classmethod
  def get(cls, format_name: str) -> "Exporter":
      if format_name not in cls._registry:
          raise KeyError(f"Unknown format: {format_name}")
      return cls._registry[format_name]()

# TODO: create JSONExporter(format_name="json") and
# CSVExporter(format_name="csv") concrete classes

exporter = Exporter.get("json")
print(exporter.export({"a": 1}))

exporter2 = Exporter.get("csv")
print(exporter2.export({"a": 1, "b": 2}))
Expected Output
{"a": 1}\na,b\n1,2
Hints

Hint 1: Pass format_name="json" in the class definition: class JSONExporter(Exporter, format_name="json").

Hint 2: JSONExporter.export() should return json.dumps(data). CSVExporter.export() should build header and value rows.

© 2026 EngineersOfAI. All rights reserved.