Python __init_subclass__ Practice Problems & Exercises
Practice: __init_subclass__
← Back to lessonEasy
Use __init_subclass__ to automatically register every subclass of Animal in a class-level list.
class Animal:
_registry = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Animal._registry.append(cls.__name__)
class Dog(Animal):
pass
class Cat(Animal):
pass
class Parrot(Animal):
pass
print(f"Registered subclasses: {Animal._registry}")Expected Output
Registered subclasses: ['Dog', 'Cat', 'Parrot']Hints
Hint 1: __init_subclass__ is a classmethod called automatically on the parent when a new subclass is defined.
Hint 2: Maintain a list on the base class (e.g., cls._registry). Append the subclass name or the subclass itself in __init_subclass__.
Build a Shape base class that requires every subclass to declare a shape_type keyword ('2d' or '3d'). Store it as a class attribute.
class Shape:
def __init_subclass__(cls, shape_type=None, **kwargs):
super().__init_subclass__(**kwargs)
if shape_type not in ("2d", "3d"):
raise TypeError(
"Shape subclass must declare shape_type='2d' or '3d'"
)
cls.shape_type = shape_type
class Circle(Shape, shape_type="2d"):
pass
class Sphere(Shape, shape_type="3d"):
pass
print(f"Circle shape_type: {Circle.shape_type}")
print(f"Sphere shape_type: {Sphere.shape_type}")
try:
class Blob(Shape):
pass
except TypeError as e:
print(f"TypeError: {e}")Expected Output
Circle shape_type: 2d
Sphere shape_type: 3d
TypeError: Shape subclass must declare shape_type='2d' or '3d'Hints
Hint 1: __init_subclass__ receives keyword arguments passed in the class definition: class Foo(Base, key=val).
Hint 2: Validate the value inside __init_subclass__ and raise TypeError if it is missing or invalid.
Write a Task base class that uses __init_subclass__ to verify that concrete subclasses implement an execute method. Raise TypeError if they do not.
class Task:
def execute(self):
raise NotImplementedError
def __init_subclass__(cls, abstract=False, **kwargs):
super().__init_subclass__(**kwargs)
if abstract:
return
# Check that cls overrides execute (not still the base version)
if cls.execute is Task.execute:
raise TypeError(f"{cls.__name__} must implement 'execute'")
class ConcreteWorker(Task):
def execute(self):
return "working"
print("ConcreteWorker created OK")
try:
class ConcreteWorker2(Task):
pass
except TypeError as e:
print(f"TypeError: {e}")Expected Output
ConcreteWorker created OK
TypeError: ConcreteWorker2 must implement 'execute'Hints
Hint 1: In __init_subclass__, check whether the subclass has overridden the required method. Use hasattr(cls, name) and getattr(cls, name) is not getattr(base, name).
Hint 2: Only enforce on non-abstract subclasses — skip classes that still define the method as NotImplemented or that end their name with 'Abstract'.
Medium
Build a parser plugin system where each Parser subclass registers itself by name. A factory method Parser.get_parser(name) returns the right subclass instance.
class Parser:
_parsers = {}
def __init_subclass__(cls, name=None, **kwargs):
super().__init_subclass__(**kwargs)
if name:
Parser._parsers[name] = cls
@classmethod
def get_parser(cls, name):
if name not in cls._parsers:
raise KeyError(f"No parser registered for '{name}'")
return cls._parsers[name]()
def parse(self, data):
raise NotImplementedError
class JSONParser(Parser, name="json"):
def parse(self, data):
import json
return json.loads(data)
class CSVParser(Parser, name="csv"):
def parse(self, data):
import csv, io
reader = csv.reader(io.StringIO(data))
return list(reader)
class XMLParser(Parser, name="xml"):
def parse(self, data):
return f"<parsed>{data}</parsed>"
print(f"Available parsers: {list(Parser._parsers.keys())}")
jp = Parser.get_parser("json")
print(f'Parsed with json: {jp.parse(\'{"key": "value"}\') }')
cp = Parser.get_parser("csv")
print(f"Parsed with csv: {cp.parse('a,b\\n1,2')}")Expected Output
Available parsers: ['json', 'csv', 'xml']
Parsed with json: {'key': 'value'}
Parsed with csv: [['a', 'b'], ['1', '2']]Hints
Hint 1: Accept a name keyword in __init_subclass__ and store the subclass in a dict keyed by name.
Hint 2: Add a classmethod get_parser(name) on the base class that looks up the registry and instantiates the right subclass.
Create a Service base class where subclasses declare a timeout keyword in their class definition. The value should propagate to sub-subclasses through normal inheritance unless explicitly overridden.
class Service:
timeout = 30 # default
def __init_subclass__(cls, timeout=None, **kwargs):
super().__init_subclass__(**kwargs)
if timeout is not None:
cls.timeout = timeout
# If not explicitly set, cls.timeout inherits from parent via MRO
class FastService(Service, timeout=5):
pass
class SlowService(Service, timeout=120):
pass
class CachedFastService(FastService):
# Inherits timeout=5 from FastService
pass
print(f"Base timeout: {Service.timeout}")
print(f"FastService timeout: {FastService.timeout}")
print(f"SlowService timeout: {SlowService.timeout}")
print(f"CachedFastService timeout: {CachedFastService.timeout}")Expected Output
Base timeout: 30
FastService timeout: 5
SlowService timeout: 120
CachedFastService timeout: 5Hints
Hint 1: __init_subclass__ can accept keyword arguments that set class attributes. These attributes are then inherited by further subclasses automatically.
Hint 2: Check cls.__dict__ (not just hasattr) when you only want the value explicitly set on that specific class — not inherited.
Write a Documented mixin that uses __init_subclass__ to enforce that every subclass has a non-empty class-level docstring.
class Documented:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
doc = cls.__dict__.get("__doc__")
if not doc or not doc.strip():
raise TypeError(
f"Class '{cls.__name__}' must have a class-level docstring"
)
class WellDocumented(Documented):
"""This class is well documented."""
pass
print("WellDocumented: OK")
try:
class LazyClass(Documented):
pass
except TypeError as e:
print(f"TypeError: {e}")Expected Output
WellDocumented: OK
TypeError: Class 'LazyClass' must have a class-level docstringHints
Hint 1: Access cls.__doc__ in __init_subclass__. It is None if no docstring was written.
Hint 2: Only enforce on leaf classes — skip if the class is itself a known abstract base.
Build a CLI command framework where each command class registers itself via __init_subclass__. A dispatcher runs the right command by name.
class Command:
_commands = {}
def __init_subclass__(cls, command=None, **kwargs):
super().__init_subclass__(**kwargs)
if command:
Command._commands[command] = cls
@classmethod
def run_command(cls, name, *args):
if name not in cls._commands:
print(f"Unknown command: {name}")
return
cmd = cls._commands[name]()
cmd.execute(*args)
def execute(self, *args):
raise NotImplementedError
class DeployCommand(Command, command="deploy"):
def execute(self, *args):
print("deploy: Deploying to production...")
class RollbackCommand(Command, command="rollback"):
def execute(self, *args):
print("rollback: Rolling back last deployment...")
class StatusCommand(Command, command="status"):
def execute(self, *args):
print("status: All systems operational")
print(f"Available commands: {list(Command._commands.keys())}")
Command.run_command("deploy")
Command.run_command("rollback")
Command.run_command("restart")Expected Output
Available commands: ['deploy', 'rollback', 'status']
deploy: Deploying to production...
rollback: Rolling back last deployment...
Unknown command: restartHints
Hint 1: Each subclass declares a command keyword. Store {command_name: cls} in a dict on the base class.
Hint 2: A classmethod run_command(name, *args) looks up the registry and calls .execute() on a fresh instance.
Hard
Design a Model base class where each schema version registers itself. A factory retrieves the right version and a latest() method returns the most recent one.
class Model:
_versions = {}
_latest_version = 0
def __init_subclass__(cls, version=None, **kwargs):
super().__init_subclass__(**kwargs)
if version is not None:
Model._versions[version] = cls
if version > Model._latest_version:
Model._latest_version = version
@classmethod
def get_version(cls, version):
if version not in cls._versions:
raise KeyError(f"No model registered for version {version}")
return cls._versions[version]
@classmethod
def latest(cls):
return cls._versions[cls._latest_version]
def serialize(self):
raise NotImplementedError
class UserV1(Model, version=1):
def __init__(self, id, name):
self.id = id
self.name = name
def serialize(self):
return {"id": self.id, "n": self.name}
class UserV2(Model, version=2):
def __init__(self, id, name):
self.id = id
self.name = name
def serialize(self):
return {"id": self.id, "name": self.name, "_v": 2}
v1_cls = Model.get_version(1)
v2_cls = Model.get_version(2)
u1 = v1_cls(1, "Alice")
u2 = v2_cls(1, "Alice")
print(f"v1 serialize: {u1.serialize()}")
print(f"v2 serialize: {u2.serialize()}")
print(f"Latest version: {Model._latest_version}")Expected Output
v1 serialize: {'id': 1, 'n': 'Alice'}
v2 serialize: {'id': 1, 'name': 'Alice', '_v': 2}
Latest version: 2Hints
Hint 1: Accept a version keyword in __init_subclass__. Store subclasses in a dict keyed by version number.
Hint 2: Track the maximum version number seen so far on the base class. A classmethod get_version(n) returns the matching subclass or raises KeyError.
Build two mixins (LoggedMixin, ThrottledMixin) that each use __init_subclass__ to configure themselves. They must cooperate via super() so a class inheriting from both works correctly.
class LoggedMixin:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.logged = True
print(f"LoggedMixin applied to {cls.__name__}")
class ThrottledMixin:
def __init_subclass__(cls, rate_limit=None, **kwargs):
super().__init_subclass__(**kwargs)
if rate_limit is not None:
cls.rate_limit = rate_limit
print(f"ThrottledMixin applied to {cls.__name__}: rate_limit={rate_limit}")
class Base(LoggedMixin, ThrottledMixin):
pass
class Service(Base, rate_limit=100):
pass
print("FinalService OK")
print(f"rate_limit: {Service.rate_limit}")
print(f"logged: {Service.logged}")Expected Output
LoggedMixin applied to Service
ThrottledMixin applied to Service: rate_limit=100
FinalService OK
rate_limit: 100
logged: TrueHints
Hint 1: Each mixin defines __init_subclass__ and must call super().__init_subclass__(**kwargs) to pass remaining kwargs up the chain.
Hint 2: Mixins can pop their own kwargs from kwargs before forwarding to super(), so each mixin handles exactly its own keyword.
Write a DepthLimited base class that raises TypeError if any subclass is created more than max_depth levels deep in the hierarchy.
class DepthLimited:
_max_depth = 2
def __init_subclass__(cls, max_depth=None, **kwargs):
super().__init_subclass__(**kwargs)
if max_depth is not None:
cls._max_depth = max_depth
# Measure depth: count classes in MRO that are subclasses of DepthLimited,
# excluding DepthLimited itself and object
depth = sum(
1 for c in cls.__mro__
if c not in (cls, DepthLimited, object)
and issubclass(c, DepthLimited)
)
# depth = number of intermediate ancestors
actual_depth = len([
c for c in cls.__mro__
if c is not object and c is not DepthLimited and issubclass(c, DepthLimited)
])
if actual_depth > cls._max_depth:
raise TypeError(
f"{cls.__name__} inheritance chain exceeds max depth of "
f"{cls._max_depth} (current depth: {actual_depth})"
)
class Level1(DepthLimited):
pass
print(f"Level1 OK (depth {len([c for c in Level1.__mro__ if c is not object and c is not DepthLimited and issubclass(c, DepthLimited)])})")
class Level2(Level1):
pass
print(f"Level2 OK (depth {len([c for c in Level2.__mro__ if c is not object and c is not DepthLimited and issubclass(c, DepthLimited)])})")
try:
class Level3(Level2):
pass
except TypeError as e:
print(f"TypeError: {e}")Expected Output
Level1 OK (depth 1)
Level2 OK (depth 2)
TypeError: MaxDepth2 inheritance chain exceeds max depth of 2 (current depth: 3)Hints
Hint 1: Walk cls.__mro__ to determine how many levels deep from the root the current class is.
Hint 2: Exclude object and the root class itself from the count. Raise TypeError if the depth exceeds the configured maximum.
Design an event bus where handler classes register themselves for specific events via __init_subclass__. A dispatch method routes events to all registered handlers.
class EventBus:
_handlers = {} # event_name -> [HandlerClass, ...]
def __init_subclass__(cls, events=None, **kwargs):
super().__init_subclass__(**kwargs)
if events:
if isinstance(events, str):
events = [events]
for event in events:
EventBus._handlers.setdefault(event, []).append(cls)
@classmethod
def dispatch(cls, event_name, **data):
handlers = cls._handlers.get(event_name, [])
for handler_cls in handlers:
handler_cls().handle(event_name, **data)
def handle(self, event_name, **data):
raise NotImplementedError
class OrderAuditHandler(EventBus, events="OrderCreated"):
def handle(self, event_name, **data):
print(f"OrderCreated -> OrderAuditHandler: order_id={data.get('order_id')}")
class OrderEmailHandler(EventBus, events="OrderCreated"):
def handle(self, event_name, **data):
print(f"OrderCreated -> OrderEmailHandler: order_id={data.get('order_id')}")
class PaymentAuditHandler(EventBus, events="PaymentReceived"):
def handle(self, event_name, **data):
print(f"PaymentReceived -> PaymentAuditHandler: amount={data.get('amount')}")
EventBus.dispatch("OrderCreated", order_id=42)
EventBus.dispatch("PaymentReceived", amount=99.99)Expected Output
OrderCreated -> OrderAuditHandler: order_id=42
OrderCreated -> OrderEmailHandler: order_id=42
PaymentReceived -> PaymentAuditHandler: amount=99.99Hints
Hint 1: Accept an events keyword (a list or tuple of event names) in __init_subclass__. Register cls against each event name in a dict.
Hint 2: The dispatch classmethod iterates all handlers registered for the event name and calls .handle(event_data) on a fresh instance of each.
