__init_subclass__ - The Modern Alternative to Metaclasses
Reading time: ~22 minutes | Level: Advanced
Before reading the explanation, predict the output of this code:
class Base:
_registry = {}
def __init_subclass__(cls, *, kind=None, **kwargs):
super().__init_subclass__(**kwargs)
if kind is not None:
Base._registry[kind] = cls
class Alpha(Base, kind="alpha"):
pass
class Beta(Base, kind="beta"):
pass
class Gamma(Base):
pass
print(list(Base._registry.keys()))
print(Base._registry["alpha"])
print(Gamma in Base._registry.values())
Click to reveal the answer
['alpha', 'beta']
<class '__main__.Alpha'>
False
__init_subclass__ is called automatically when a subclass is defined. The kind keyword argument is extracted from the class statement itself. Gamma did not pass kind=, so it was not registered. No instances were ever created - this all happened at class definition time.
What You Will Learn
- What
__init_subclass__is, when it fires, and what arguments it receives - How to use keyword arguments in
classstatements and route them to__init_subclass__ - How to build plugin registries that auto-register subclasses at definition time
- How to validate class definitions before any instance is created
- When to use
__init_subclass__instead of metaclasses (and the rare cases where you still need a metaclass) - How
super().__init_subclass__()cooperates with multiple inheritance - How Python's standard library uses this protocol in
enum.Enumandtyping.Protocol
Prerequisites
- Solid understanding of Python classes, inheritance, and MRO
- Familiarity with descriptors and the
__new__/__init__distinction on classes - Understanding of metaclasses (at least conceptually - this lesson shows when you can avoid them)
- Comfort with
super()and cooperative multiple inheritance
Part 1 - What __init_subclass__ Is
Introduced in Python 3.6 via PEP 487, __init_subclass__ is a hook method defined on a parent class that is called automatically every time a new subclass of that parent is created.
The critical mental model: __init_subclass__ runs at class definition time, not at instantiation time. It receives the newly created subclass as cls.
class Base:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
print(f"New subclass created: {cls.__name__}")
class Child(Base): # prints: New subclass created: Child
pass
class Grandchild(Child): # prints: New subclass created: Grandchild
pass
The call chain is:
- Python encounters a
class Child(Base):statement - The class body executes, producing a namespace
type.__new__creates the class object__set_name__is called on all descriptors in the namespaceBase.__init_subclass__(Child)is called- The class object is bound to
Childin the enclosing scope
__init_subclass__ is NOT called on the class that defines it. Base.__init_subclass__ never fires for Base itself - only for subclasses of Base.
Part 2 - Basic Usage Patterns
Subclass Registration
The most common use: automatically tracking all subclasses of a base class.
class Serializer:
_formats = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Use the class name as the format key
fmt = cls.__name__.replace("Serializer", "").lower()
if fmt:
Serializer._formats[fmt] = cls
@classmethod
def for_format(cls, fmt):
"""Factory method - get serializer by format name."""
serializer_cls = cls._formats.get(fmt)
if serializer_cls is None:
raise ValueError(f"Unknown format: {fmt!r}. Available: {list(cls._formats)}")
return serializer_cls()
class JsonSerializer(Serializer):
def serialize(self, data):
import json
return json.dumps(data)
class XmlSerializer(Serializer):
def serialize(self, data):
return f"<data>{data}</data>"
# Usage - no manual registration needed
s = Serializer.for_format("json")
print(s.serialize({"key": "value"})) # {"key": "value"}
print(Serializer._formats)
# {'json': <class '__main__.JsonSerializer'>, 'xml': <class '__main__.XmlSerializer'>}
Setting Defaults on Subclasses
class Model:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Every model subclass gets a default table_name if not set
if not hasattr(cls, "table_name") or "table_name" not in cls.__dict__:
cls.table_name = cls.__name__.lower() + "s"
class User(Model):
pass
class Product(Model):
table_name = "inventory"
print(User.table_name) # users
print(Product.table_name) # inventory
Part 3 - Keyword Arguments in Class Statements
This is the feature that makes __init_subclass__ truly powerful. You can pass keyword arguments directly in the class statement, and they flow to __init_subclass__.
class Base:
def __init_subclass__(cls, *, register=True, tag=None, **kwargs):
super().__init_subclass__(**kwargs)
cls._tag = tag
if register:
print(f"Registered {cls.__name__} with tag={tag!r}")
else:
print(f"Skipped registration of {cls.__name__}")
class Alpha(Base, tag="core", register=True):
pass
# Registered Alpha with tag='core'
class Beta(Base, register=False):
pass
# Skipped registration of Beta
class Gamma(Base, tag="experimental"):
pass
# Registered Gamma with tag='experimental'
The syntax class Alpha(Base, tag="core") looks unusual if you have never seen it. The keyword arguments after the base classes are intercepted and forwarded to __init_subclass__.
:::danger Common Mistake
If you define __init_subclass__ with specific keyword parameters but forget **kwargs, you will break compatibility with multiple inheritance. Always accept and forward **kwargs:
# WRONG - breaks if another class in the MRO also uses __init_subclass__
def __init_subclass__(cls, *, tag=None):
super().__init_subclass__() # cannot forward unknown kwargs
# CORRECT - cooperative
def __init_subclass__(cls, *, tag=None, **kwargs):
super().__init_subclass__(**kwargs)
:::
How Keyword Arguments Flow
When Python sees class Foo(Base, option="x"), the following happens:
Baseis identified as a base classoption="x"is identified as a keyword argument (not a base class)- After class creation,
Base.__init_subclass__(cls=Foo, option="x")is called
The class statement can also accept metaclass= as a keyword argument - Python handles that separately and does not pass it to __init_subclass__.
# These keyword args have different destinations:
class Foo(Base, metaclass=type, tag="core"):
pass
# metaclass= → goes to type() for class creation
# tag= → goes to __init_subclass__
Part 4 - Building a Plugin Registry
Here is a production-quality plugin registry built entirely with __init_subclass__:
from collections import OrderedDict
class PluginBase:
"""Base class for a plugin system.
Subclasses auto-register. Supports enable/disable, ordered discovery,
and lookup by name.
"""
_plugins: OrderedDict = OrderedDict()
def __init_subclass__(cls, *, name=None, enabled=True, **kwargs):
super().__init_subclass__(**kwargs)
plugin_name = name or cls.__name__.lower()
cls._plugin_name = plugin_name
cls._plugin_enabled = enabled
PluginBase._plugins[plugin_name] = cls
@classmethod
def get_plugin(cls, name):
"""Retrieve a plugin class by name."""
plugin = cls._plugins.get(name)
if plugin is None:
available = [n for n, p in cls._plugins.items() if p._plugin_enabled]
raise KeyError(f"No plugin {name!r}. Available: {available}")
if not plugin._plugin_enabled:
raise RuntimeError(f"Plugin {name!r} is disabled")
return plugin
@classmethod
def enabled_plugins(cls):
"""Return all enabled plugins in registration order."""
return OrderedDict(
(name, p) for name, p in cls._plugins.items() if p._plugin_enabled
)
@classmethod
def discover(cls):
"""Print all registered plugins and their status."""
for name, plugin in cls._plugins.items():
status = "enabled" if plugin._plugin_enabled else "DISABLED"
print(f" [{status}] {name} -> {plugin.__qualname__}")
# --- Plugin definitions (could be in separate files) ---
class MarkdownRenderer(PluginBase, name="markdown"):
def render(self, text):
return f"**{text}**"
class HtmlRenderer(PluginBase, name="html"):
def render(self, text):
return f"<p>{text}</p>"
class LatexRenderer(PluginBase, name="latex", enabled=False):
def render(self, text):
return f"\\textbf{{{text}}}"
# --- Usage ---
PluginBase.discover()
# [enabled] markdown -> MarkdownRenderer
# [enabled] html -> HtmlRenderer
# [DISABLED] latex -> LatexRenderer
renderer_cls = PluginBase.get_plugin("html")
renderer = renderer_cls()
print(renderer.render("Hello")) # <p>Hello</p>
print(list(PluginBase.enabled_plugins().keys())) # ['markdown', 'html']
:::tip Production Consideration
In a real plugin system, plugins live in separate packages. The registration still works because importing the module triggers class creation, which triggers __init_subclass__. You can use importlib.metadata.entry_points() (covered in the Import Hooks lesson) to discover and import plugin packages.
:::
Part 5 - Validation at Class Definition Time
One of the most valuable applications: rejecting invalid subclasses before any instance is created. Bugs surface immediately at import time rather than deep in a runtime call stack.
class Endpoint:
"""Base for HTTP endpoint handlers.
Subclasses MUST define `path` and `method`, and `method` must be
a valid HTTP method.
"""
VALID_METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Skip validation for abstract intermediate classes
if getattr(cls, "__abstract__", False):
return
# Validate required attributes
if "path" not in cls.__dict__:
raise TypeError(
f"{cls.__name__} must define a 'path' class attribute"
)
if "method" not in cls.__dict__:
raise TypeError(
f"{cls.__name__} must define a 'method' class attribute"
)
if cls.method not in Endpoint.VALID_METHODS:
raise ValueError(
f"{cls.__name__}.method = {cls.method!r} is not valid. "
f"Must be one of {Endpoint.VALID_METHODS}"
)
# Validate handler method exists
if not callable(getattr(cls, "handle", None)):
raise TypeError(
f"{cls.__name__} must define a 'handle' method"
)
# This is fine:
class GetUsers(Endpoint):
path = "/users"
method = "GET"
def handle(self, request):
return [{"name": "Alice"}]
# This fails at class definition time - no instance needed:
try:
class BadEndpoint(Endpoint):
path = "/broken"
method = "YEET"
def handle(self, request):
pass
except ValueError as e:
print(e)
# BadEndpoint.method = 'YEET' is not valid. Must be one of {...}
# This also fails:
try:
class IncompleteEndpoint(Endpoint):
path = "/incomplete"
except TypeError as e:
print(e)
# IncompleteEndpoint must define a 'method' class attribute
Skipping Abstract Intermediates
Notice the __abstract__ guard above. Without it, intermediate base classes that are not concrete endpoints would also be validated. A clean pattern:
class JsonEndpoint(Endpoint):
__abstract__ = True # skip validation for this layer
def serialize(self, data):
import json
return json.dumps(data)
# JsonEndpoint is not validated - it is an intermediate base.
# Concrete subclasses still must define path, method, and handle:
class ListProducts(JsonEndpoint):
path = "/products"
method = "GET"
def handle(self, request):
return self.serialize([{"id": 1, "name": "Widget"}])
This pattern is far more ergonomic than metaclass-based validation. You do not need to understand type.__new__ or worry about metaclass conflicts. The base class itself controls the validation logic.
Part 6 - __init_subclass__ vs Metaclasses
When should you use each? Here is the decision matrix:
| Capability | __init_subclass__ | Metaclass |
|---|---|---|
| Register subclasses | Yes | Yes |
| Validate class definitions | Yes | Yes |
| Add/modify attributes on subclass | Yes | Yes |
Accept keyword args from class statement | Yes | Yes |
Control class namespace (e.g., OrderedDict) | No | Yes (__prepare__) |
| Intercept attribute access on the class itself | No | Yes (__getattr__ on metaclass) |
| Change the type of the class object | No | Yes (by definition) |
Customize isinstance/issubclass behavior | No | Yes (__instancecheck__, __subclasscheck__) |
| Multiple base classes with different hooks | Works naturally | Metaclass conflict if bases use different metaclasses |
The rule:
Reach for
__init_subclass__first. Only use a metaclass when you need capabilities that__init_subclass__cannot provide - custom namespaces, class-level__getattr__, or changing the fundamental type of the class object.
The Metaclass Conflict Problem
One of the strongest arguments for __init_subclass__: it avoids the metaclass conflict entirely.
# Two libraries with different metaclasses:
class MetaA(type):
pass
class MetaB(type):
pass
class A(metaclass=MetaA):
pass
class B(metaclass=MetaB):
pass
# This is ILLEGAL - metaclass conflict:
try:
class C(A, B):
pass
except TypeError as e:
print(e)
# metaclass conflict: the metaclass of a derived class must be a
# (non-strict) subclass of the metaclasses of all its bases
# With __init_subclass__, no conflict:
class MixinA:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
print(f"MixinA sees {cls.__name__}")
class MixinB:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
print(f"MixinB sees {cls.__name__}")
class Combined(MixinA, MixinB): # works perfectly
pass
# MixinB sees Combined
# MixinA sees Combined
Part 7 - Cooperating with super().__init_subclass__()
The super() call inside __init_subclass__ is not optional politeness - it is essential for cooperative multiple inheritance. Without it, only the first __init_subclass__ in the MRO executes.
class Registerable:
_registry = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Registerable._registry.append(cls.__name__)
print(f"Registerable: registered {cls.__name__}")
class Validated:
def __init_subclass__(cls, *, required_attrs=(), **kwargs):
super().__init_subclass__(**kwargs)
for attr in required_attrs:
if attr not in cls.__dict__:
raise TypeError(f"{cls.__name__} missing required attribute: {attr!r}")
print(f"Validated: {cls.__name__} passed validation")
class Entity(Registerable, Validated):
"""Inherits both hooks - both fire for Entity's subclasses."""
pass
class User(Entity, required_attrs=("username_field",)):
username_field = "email"
# Validated: User passed validation
# Registerable: registered User
print(Registerable._registry)
# ['Entity', 'User']
MRO Determines Call Order
The __init_subclass__ calls follow the MRO, just like any other cooperative method. For class User(Entity, required_attrs=...):
- Python calls
Entity.__init_subclass__- which is not defined, so MRO continues Registerable.__init_subclass__fires, then callssuper()Validated.__init_subclass__fires (next in MRO), then callssuper()object.__init_subclass__fires (no-op)
:::danger Forgetting super() Breaks the Chain
class BrokenMixin:
def __init_subclass__(cls, **kwargs):
# WRONG: no super() call - stops the chain
print(f"BrokenMixin sees {cls.__name__}")
class GoodMixin:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
print(f"GoodMixin sees {cls.__name__}")
class Combined(BrokenMixin, GoodMixin):
pass
# Only prints: BrokenMixin sees Combined
# GoodMixin.__init_subclass__ is NEVER called
:::
Real-World Usage in Python's Standard Library
enum.Enum
The Enum class uses __init_subclass__ (in combination with a metaclass) to intercept subclass creation. When you write:
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
The EnumMeta metaclass handles the heavy lifting (custom namespace via __prepare__), but __init_subclass__ is available for your own Enum extensions to hook into subclass creation without interfering with EnumMeta.
typing.Protocol
Since Python 3.8, Protocol uses __init_subclass__ to mark subclasses as protocol implementations and validate structural subtyping at definition time:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("Drawing circle")
print(isinstance(Circle(), Drawable)) # True - structural subtyping
Under the hood, Protocol.__init_subclass__ sets internal flags like _is_protocol that control how isinstance checks work.
dataclasses
While @dataclass is a decorator, it could have been implemented using __init_subclass__ on a base class. The attrs library's attrs.define and Pydantic's BaseModel both use __init_subclass__ internally to process field definitions when a model subclass is created.
Key Takeaways
__init_subclass__is called on the parent class when a subclass is defined, receiving the new subclass ascls- It fires at class definition time, not instantiation time - errors surface at import
- Keyword arguments in
class Foo(Base, key="value")flow directly to__init_subclass__ - Always accept
**kwargsand callsuper().__init_subclass__(**kwargs)for cooperative behavior - Use it for registration, validation, setting defaults, and plugin discovery
- Prefer
__init_subclass__over metaclasses unless you need__prepare__, class-level__getattr__, or customisinstancebehavior - It avoids the metaclass conflict problem that arises with multiple inheritance
Graded Practice Challenges
Level 1 - Predict the Output
Question 1:
class Base:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.level = getattr(cls, "level", 0) + 1
class A(Base):
pass
class B(A):
pass
class C(B):
pass
print(A.level, B.level, C.level)
Answer
1 2 3
Each time a subclass is created, __init_subclass__ fires. A inherits level=0 from nothing (getattr default), so gets 0+1=1. B inherits level=1 from A, so gets 1+1=2. C inherits level=2 from B, so gets 2+1=3. Each class gets its own level attribute set in __dict__.
Question 2:
class Registry:
_all = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Registry._all.append(cls)
class A(Registry):
pass
class B(Registry):
pass
class C(A):
pass
print(len(Registry._all))
print(Registry._all[-1].__name__)
Answer
3
C
__init_subclass__ fires for every subclass, including indirect ones. A, B, and C are all subclasses of Registry. C inherits from A which inherits from Registry, so it still triggers the hook.
Question 3:
class Base:
def __init_subclass__(cls, *, prefix="default", **kwargs):
super().__init_subclass__(**kwargs)
cls.full_name = f"{prefix}:{cls.__name__}"
class Alpha(Base, prefix="core"):
pass
class Beta(Base):
pass
print(Alpha.full_name)
print(Beta.full_name)
Answer
core:Alpha
default:Beta
Alpha passes prefix="core" explicitly. Beta uses the default parameter value "default".
Level 2 - Debug Challenge
The following code is supposed to build a middleware stack where each middleware class must define a process method, but it has a bug. Find and fix it.
class Middleware:
_stack = []
def __init_subclass__(cls, *, order=0, **kwargs):
# Bug is somewhere in this method
cls._order = order
Middleware._stack.append(cls)
Middleware._stack.sort(key=lambda c: c._order)
if not hasattr(cls, "process"):
raise TypeError(f"{cls.__name__} must define a process() method")
class LoggingMiddleware(Middleware, order=1):
def process(self, request):
print(f"LOG: {request}")
return request
class AuthMiddleware(Middleware, order=0):
def process(self, request):
print(f"AUTH: {request}")
return request
class CompressionMiddleware(Middleware, order=2):
def process(self, request):
print(f"COMPRESS: {request}")
return request
Hint
Try creating a subclass that inherits from LoggingMiddleware. What happens? Also, what happens if two base classes both use __init_subclass__?
Solution
Two bugs:
-
Missing
super().__init_subclass__(**kwargs)- this breaks cooperative multiple inheritance. -
The
hasattr(cls, "process")check is wrong.hasattrtraverses the MRO, so a subclass ofLoggingMiddlewarewould pass the check even without defining its ownprocess. You should checkcls.__dict__if you want each class to define its own, or usecallable(getattr(cls, "process", None))if inherited is acceptable. More importantly, the check fires for intermediate abstract classes too.
Fixed version:
class Middleware:
_stack = []
def __init_subclass__(cls, *, order=0, abstract=False, **kwargs):
super().__init_subclass__(**kwargs)
cls._order = order
if not abstract:
if not callable(getattr(cls, "process", None)):
raise TypeError(f"{cls.__name__} must define a process() method")
Middleware._stack.append(cls)
Middleware._stack.sort(key=lambda c: c._order)
Level 3 - Design Challenge
Design a Component base class for a UI framework with these requirements:
- Every concrete subclass must declare a
tagkeyword argument in the class statement (e.g.,class Button(Component, tag="button")) - Duplicate tags are rejected at class definition time with a clear error
- An abstract subclass can skip the
tagrequirement by passingabstract=True - All registered components are discoverable via
Component.by_tag("button") - Components must define a
render(self) -> strmethod - validated at definition time - Support ordered iteration over all components in registration order
Your solution should be approximately 30-50 lines for the base class. Test it with at least three concrete components and one abstract intermediate class.
Solution Sketch
from collections import OrderedDict
class Component:
_components: OrderedDict = OrderedDict()
def __init_subclass__(cls, *, tag=None, abstract=False, **kwargs):
super().__init_subclass__(**kwargs)
if abstract:
return
if tag is None:
raise TypeError(
f"{cls.__name__} must specify tag= (e.g., class {cls.__name__}"
f"(Component, tag='my-tag'))"
)
if tag in Component._components:
existing = Component._components[tag].__name__
raise TypeError(
f"Duplicate tag {tag!r}: already used by {existing}"
)
if not callable(getattr(cls, "render", None)):
raise TypeError(f"{cls.__name__} must define a render() method")
cls._tag = tag
Component._components[tag] = cls
@classmethod
def by_tag(cls, tag):
comp = cls._components.get(tag)
if comp is None:
raise KeyError(f"No component with tag={tag!r}")
return comp
@classmethod
def all_components(cls):
return list(cls._components.items())
class StyledComponent(Component, abstract=True):
"""Abstract intermediate - no tag required."""
def apply_style(self, style_dict):
return "; ".join(f"{k}: {v}" for k, v in style_dict.items())
class Button(StyledComponent, tag="button"):
def render(self):
return "<button>Click</button>"
class TextInput(StyledComponent, tag="input"):
def render(self):
return "<input type='text' />"
class Heading(Component, tag="h1"):
def render(self):
return "<h1>Title</h1>"
# Test
print(Component.by_tag("button")) # <class 'Button'>
print(Component.all_components()) # [('button', ...), ('input', ...), ('h1', ...)]
print(Button().render()) # <button>Click</button>
What's Next
In the next lesson, we explore __set_name__ - the descriptor naming protocol that eliminated one of Python's most common metaprogramming redundancies and enabled self-configuring descriptors.
