__init_subclass__¶
__init_subclass__ (Python 3.6+) is a hook that runs whenever a class is subclassed. It provides a simpler alternative to metaclasses for many common use cases — but it has hard limits.
Mental Model
__init_subclass__ is a parent class's callback that fires every time someone writes class Child(Parent). Think of it as a registration desk: the parent inspects each new child at birth, can enforce rules (require certain methods), inject behavior (auto-register plugins), or reject the child entirely. It covers 80% of metaclass use cases with none of the complexity.
Hard Limits — what __init_subclass__ cannot do
__init_subclass__ runs after the class object already exists. It cannot:
- Modify the class namespace before creation (use
__prepare__in a metaclass) - Affect method resolution order (MRO)
- Intercept or control instance creation (use
__call__in a metaclass)
text
class statement → metaclass.__new__ → class object exists → __init_subclass__
If you need to act before the class is fully formed, you need a metaclass.
The super() rule
Always call super().__init_subclass__(**kwargs) in your implementation. This
ensures cooperative multiple inheritance works correctly — without it, sibling
hooks in the MRO chain will be silently skipped.
python
class Base:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Called when any class inherits from Base
Basic Usage¶
```python class Plugin: def init_subclass(cls, kwargs): super().init_subclass(kwargs) print(f"New plugin registered: {cls.name}")
class AudioPlugin(Plugin): pass
Output: New plugin registered: AudioPlugin¶
class VideoPlugin(Plugin): pass
Output: New plugin registered: VideoPlugin¶
```
How It Works¶
When you define a subclass:
python
class Child(Parent):
pass
Python automatically calls:
python
Parent.__init_subclass__(Child)
The hook receives:
cls: The newly created subclass (not the parent)**kwargs: Any keyword arguments from the class definition
Passing Arguments¶
You can pass arguments in the class definition:
```python class Serializable: def init_subclass(cls, format="json", kwargs): super().init_subclass(kwargs) cls._format = format print(f"{cls.name} uses {format} format")
class User(Serializable, format="xml"): pass
Output: User uses xml format¶
class Product(Serializable): # Uses default pass
Output: Product uses json format¶
print(User._format) # xml print(Product._format) # json ```
Practical Examples¶
Plugin Registration¶
```python class Plugin: _registry = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Register plugin by name
Plugin._registry[cls.__name__] = cls
@classmethod
def get_plugin(cls, name):
return cls._registry.get(name)
class ImageProcessor(Plugin): def process(self, data): return f"Processing image: {data}"
class TextProcessor(Plugin): def process(self, data): return f"Processing text: {data}"
Access registered plugins¶
print(Plugin._registry)
{'ImageProcessor': , 'TextProcessor': }¶
processor = Plugin.get_plugin("ImageProcessor")() print(processor.process("photo.jpg")) ```
Validation¶
```python class ValidatedModel: required_fields = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Check that required fields are defined
for field in cls.required_fields:
if not hasattr(cls, field):
raise TypeError(
f"{cls.__name__} must define '{field}'"
)
class UserModel(ValidatedModel): required_fields = ['name', 'email'] name = str email = str # ✓ Valid - has both required fields
class InvalidModel(ValidatedModel):¶
required_fields = ['name', 'email']¶
name = str¶
# TypeError: InvalidModel must define 'email'¶
```
Automatic Method Addition¶
Illustrative example — not production-safe
The AutoRepr pattern below inspects __init__ parameters via inspect.signature.
This breaks with inheritance (picks up the wrong __init__), does not handle
*args/**kwargs, and ignores dataclasses, slots, and descriptors. For production
use, prefer @dataclass (which generates __repr__ correctly) or write an explicit
__repr__.
```python class AutoRepr: def init_subclass(cls, kwargs): super().init_subclass(kwargs)
# Add __repr__ based on __init__ parameters
if hasattr(cls, '__init__'):
import inspect
sig = inspect.signature(cls.__init__)
params = [p for p in sig.parameters if p != 'self']
def __repr__(self):
values = ', '.join(
f"{p}={getattr(self, p)!r}"
for p in params
if hasattr(self, p)
)
return f"{self.__class__.__name__}({values})"
cls.__repr__ = __repr__
class Person(AutoRepr): def init(self, name, age): self.name = name self.age = age
p = Person("Alice", 30) print(p) # Person(name='Alice', age=30) ```
Configuration Inheritance¶
```python class Configurable: _config = {}
def __init_subclass__(cls, **config):
super().__init_subclass__()
# Inherit parent config and update with new values
cls._config = {**cls._config, **config}
class BaseService(Configurable, timeout=30, retries=3): pass
class DatabaseService(BaseService, timeout=60): pass
class CacheService(BaseService, retries=5): pass
print(BaseService._config) # {'timeout': 30, 'retries': 3} print(DatabaseService._config) # {'timeout': 60, 'retries': 3} print(CacheService._config) # {'timeout': 30, 'retries': 5} ```
Enforcing Abstract Methods¶
```python class Interface: _required_methods = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Skip abstract classes
if getattr(cls, '_abstract', False):
return
# Check all required methods are implemented
missing = []
for method in cls._required_methods:
if not callable(getattr(cls, method, None)):
missing.append(method)
if missing:
raise TypeError(
f"{cls.__name__} must implement: {', '.join(missing)}"
)
class Repository(Interface): _abstract = True _required_methods = ['get', 'save', 'delete']
class UserRepository(Repository): def get(self, id): return {"id": id}
def save(self, entity):
pass
def delete(self, id):
pass
✓ Valid¶
class BadRepository(Repository):¶
def get(self, id):¶
return¶
TypeError: BadRepository must implement: save, delete¶
```
With Multiple Inheritance¶
```python class A: def init_subclass(cls, kwargs): super().init_subclass(kwargs) print(f"A.init_subclass for {cls.name}")
class B: def init_subclass(cls, kwargs): super().init_subclass(kwargs) print(f"B.init_subclass for {cls.name}")
class C(A, B): pass
Output:¶
B.init_subclass for C¶
A.init_subclass for C¶
```
Note how the MRO determines the order: B.__init_subclass__ runs before
A.__init_subclass__ because Python traverses the MRO from right to left when
calling cooperative super() chains.
init_subclass vs Metaclass¶
| Feature | __init_subclass__ |
Metaclass |
|---|---|---|
| Complexity | Simple | Complex |
| Called when | Class is subclassed | Class is created |
| Access to | Subclass only | Full creation process |
| Modify namespace | No | Yes (via __prepare__) |
| Control instantiation | No | Yes (via __call__) |
| Use case | Registration, validation | DSLs, ORMs |
Decision Rule
Use __init_subclass__ when you only need the finished class — registration,
validation, adding attributes. Use a metaclass when you need to control how the
class is built — custom namespace (__prepare__), intercepting instance creation
(__call__), or transforming the class body before the class object exists.
When to Use init_subclass¶
- Registering subclasses
- Validating class definitions
- Adding methods or attributes
- Simple class customization
When to Use Metaclass¶
- Modifying class before creation
- Controlling instance creation
- Custom namespace handling
- Complex framework behavior
Common Patterns¶
Optional Hook¶
```python class OptionalHook: def init_subclass(cls, register=True, kwargs): super().init_subclass(kwargs) if register: cls._registered = True else: cls._registered = False
class Registered(OptionalHook): pass
class NotRegistered(OptionalHook, register=False): pass
print(Registered._registered) # True print(NotRegistered._registered) # False ```
Chained Hooks¶
```python class Logger: def init_subclass(cls, kwargs): super().init_subclass(kwargs) print(f"[LOG] Created: {cls.name}")
class Validator: def init_subclass(cls, kwargs): super().init_subclass(kwargs) print(f"[VALIDATE] Checking: {cls.name}")
class MyClass(Logger, Validator): pass
[VALIDATE] Checking: MyClass¶
[LOG] Created: MyClass¶
```
Summary¶
| Feature | Example |
|---|---|
| Basic hook | def __init_subclass__(cls, **kwargs): |
| With arguments | class Child(Parent, arg=value): |
| Always call super | super().__init_subclass__(**kwargs) |
| Access subclass | cls parameter |
Key Takeaways:
__init_subclass__is called when a class is subclassed- Simpler alternative to metaclasses for many use cases
- Always call
super().__init_subclass__(**kwargs) - Can receive keyword arguments from class definition
- Use for registration, validation, and automatic setup
- Prefer over metaclasses unless you need
__prepare__or__call__
Runnable Example: singleton_metaclass_example.py¶
```python """ Metaclass Example: Thread-Safe Singleton
A metaclass is a "class of a class" - it controls how classes are created and how instances are constructed.
This tutorial implements a singleton pattern using a metaclass, where call on the metaclass intercepts instance creation.
Topics covered: - Custom metaclass (inheriting from type) - init on metaclass (called when class is defined) - call on metaclass (called when class() is invoked) - Thread-safe double-checked locking - Comparison with decorator approach
Based on concepts from Python-100-Days example18 and ch06/metaclasses materials. """
import threading
=============================================================================¶
Example 1: Singleton Metaclass¶
=============================================================================¶
class SingletonMeta(type): """Metaclass that makes any class using it a singleton.
How it works:
1. When the class is DEFINED (class Foo(metaclass=SingletonMeta):),
SingletonMeta.__init__ runs, initializing _instance and _lock.
2. When Foo() is CALLED to create an instance,
SingletonMeta.__call__ runs instead of the normal type.__call__.
3. __call__ checks if an instance already exists (thread-safely).
"""
def __init__(cls, *args, **kwargs):
"""Called when the class is first defined (not when instantiated)."""
cls._instance = None
cls._lock = threading.Lock()
super().__init__(*args, **kwargs)
def __call__(cls, *args, **kwargs):
"""Called every time cls() is invoked (instead of creating new instance).
Uses double-checked locking for thread safety:
1. Fast check without lock (avoids lock overhead after first creation)
2. Acquire lock and check again (prevents race condition)
"""
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__call__(*args, **kwargs)
return cls._instance
=============================================================================¶
Example 2: Using the Singleton Metaclass¶
=============================================================================¶
class DatabaseConnection(metaclass=SingletonMeta): """Database connection that should only exist once.
Using metaclass=SingletonMeta ensures that DatabaseConnection()
always returns the same instance.
"""
def __init__(self, host: str = "localhost", port: int = 5432):
self.host = host
self.port = port
self.connected = True
def __str__(self):
return f"DB({self.host}:{self.port})"
class AppLogger(metaclass=SingletonMeta): """Application logger (separate singleton from DatabaseConnection)."""
def __init__(self, name: str = "app"):
self.name = name
self.entries: list[str] = []
def log(self, message: str):
self.entries.append(message)
def __str__(self):
return f"Logger('{self.name}', {len(self.entries)} entries)"
=============================================================================¶
Example 3: Demonstrating Singleton Behavior¶
=============================================================================¶
def demo_singleton(): """Show that the metaclass enforces singleton behavior.""" print("=== Singleton Metaclass Demo ===")
# First call creates the instance
db1 = DatabaseConnection("postgres.example.com", 5432)
# Second call returns the SAME instance (args ignored)
db2 = DatabaseConnection("mysql.example.com", 3306)
# Even __call__ returns the same instance
db3 = DatabaseConnection.__call__("oracle.example.com", 1521)
print(f"db1: {db1}")
print(f"db2: {db2}")
print(f"db3: {db3}")
print(f"db1 is db2: {db1 is db2}")
print(f"db1 is db3: {db1 is db3}")
print()
# Different classes have independent singletons
logger = AppLogger("main")
print(f"logger: {logger}")
print(f"logger is db1: {logger is db1}")
print()
=============================================================================¶
Example 4: isinstance() Works (Unlike Decorator Approach)¶
=============================================================================¶
def demo_isinstance(): """Show that isinstance works correctly with metaclass singleton.""" print("=== isinstance() Works Correctly ===")
db = DatabaseConnection()
print(f"isinstance(db, DatabaseConnection): {isinstance(db, DatabaseConnection)}")
print(f"type(db): {type(db).__name__}")
print(f"type(DatabaseConnection): {type(DatabaseConnection).__name__}")
print()
print("Note: With a decorator singleton, isinstance() would fail")
print("because the decorator replaces the class with a function.")
print("The metaclass approach preserves the class identity.")
print()
=============================================================================¶
Example 5: The Metaclass Chain¶
=============================================================================¶
def demo_metaclass_chain(): """Visualize the metaclass relationship.""" print("=== Metaclass Chain ===") print(""" Normal chain: instance -> class -> type (default metaclass) Singleton chain: instance -> class -> SingletonMeta -> type
The chain:
- db = DatabaseConnection() # db is an instance
- type(db) is DatabaseConnection # class of db
- type(DatabaseConnection) is SingletonMeta # metaclass
- type(SingletonMeta) is type # meta-metaclass (always type)
""")
db = DatabaseConnection()
print(f"Instance: {db}")
print(f"Class: {type(db).__name__}")
print(f"Metaclass: {type(type(db)).__name__}")
print(f"Meta-meta: {type(type(type(db))).__name__}")
=============================================================================¶
Main¶
=============================================================================¶
if name == 'main': demo_singleton() demo_isinstance() demo_metaclass_chain() ```
Exercises¶
Exercise 1.
Create a base class Plugin that uses __init_subclass__ to automatically register all subclasses into a class-level _registry dictionary (mapping the class name to the class). Create three plugin subclasses and print the registry without any manual registration code.
Solution to Exercise 1
class Plugin:
_registry = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Plugin._registry[cls.__name__] = cls
class AuthPlugin(Plugin):
pass
class CachePlugin(Plugin):
pass
class LogPlugin(Plugin):
pass
print(Plugin._registry)
# {'AuthPlugin': <class 'AuthPlugin'>, 'CachePlugin': ..., 'LogPlugin': ...}
Exercise 2.
Write a base class ValidatedModel where __init_subclass__ checks that every subclass defines a required_fields class attribute (a list of strings). If missing, raise TypeError. Create a valid subclass and an invalid one to demonstrate the validation.
Solution to Exercise 2
class ValidatedModel:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if not hasattr(cls, 'required_fields'):
raise TypeError(f"{cls.__name__} must define 'required_fields'")
class User(ValidatedModel):
required_fields = ["name", "email"]
print(User.required_fields) # ['name', 'email']
try:
class BadModel(ValidatedModel):
pass # Missing required_fields
except TypeError as e:
print(f"Error: {e}")
# Error: BadModel must define 'required_fields'
Exercise 3.
Build a Serializable base class where __init_subclass__ accepts a format keyword argument (e.g., class MyData(Serializable, format="json")). Store the format on the subclass as _format. Add a serialize() method that prints the format. Show that different subclasses can declare different formats.
Solution to Exercise 3
class Serializable:
def __init_subclass__(cls, format="text", **kwargs):
super().__init_subclass__(**kwargs)
cls._format = format
def serialize(self):
print(f"Serializing {self.__class__.__name__} as {self._format}")
class JsonData(Serializable, format="json"):
pass
class XmlData(Serializable, format="xml"):
pass
class PlainData(Serializable): # Uses default "text"
pass
JsonData().serialize() # Serializing JsonData as json
XmlData().serialize() # Serializing XmlData as xml
PlainData().serialize() # Serializing PlainData as text