Python Abstract Base Classes — Enforcing: Practice Problems & Exercises
Practice: Abstract Base Classes — Enforcing Interfaces at Engineering Depth
← Back to lessonEasy
Implement Circle.area() to satisfy the Shape ABC. The area of a circle is pi * r squared.
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.53981633974483Hints
Hint 1: Use import math and return math.pi * self.radius ** 2.
Hint 2: Forgetting to implement area() would raise TypeError on instantiation.
Verify that Python prevents direct instantiation of an ABC by catching the TypeError it raises.
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.
Implement JSONSerializer by providing concrete implementations for both abstract methods defined in Serializer.
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).
Verify that isinstance and issubclass work correctly with ABC hierarchies. Both Dog and Cat should be recognised as Animal instances.
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)) # TrueSolution
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\nTrueHints
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
Implement DevConfig to satisfy the Config ABC by providing concrete @property implementations for both abstract properties.
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\nTrueHints
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.
Register LegacyWidget as a virtual subclass of Drawable using register(). The widget should then pass isinstance checks without inheriting from Drawable.
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 widgetHints
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.
Implement the two required abstract methods (__getitem__ and __len__) of collections.abc.Sequence. The mixin methods (__contains__, __iter__) are provided for free.
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]\nTrueHints
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.
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.
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 flyingHints
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
Implement CaseInsensitiveDict using collections.abc.MutableMapping. Keys should be normalised to lowercase internally so d["Name"] and d["name"] refer to the same entry.
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\n1Hints
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.
Implement __subclasshook__ so that any class with a to_string method is automatically considered a subclass of Printable, without needing to call register().
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)) # FalseSolution
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\nFalseHints
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).
Implement a self-registering plugin architecture using __init_subclass__. Create JSONExporter and CSVExporter that register themselves automatically when defined.
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,2Hints
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.
