__init_subclass__¶
__init_subclass__ (Python 3.6+) is a hook that's called whenever a class is subclassed. It provides a simpler alternative to metaclasses for many common use cases.
class Base:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Called when any class inherits from Base
Basic Usage¶
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:
class Child(Parent):
pass
Python automatically calls:
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:
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¶
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': <class 'ImageProcessor'>, 'TextProcessor': <class 'TextProcessor'>}
processor = Plugin.get_plugin("ImageProcessor")()
print(processor.process("photo.jpg"))
Validation¶
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¶
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¶
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¶
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 {"id": id}
# TypeError: BadRepository must implement: save, delete
With Multiple Inheritance¶
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
Important: Always call super().__init_subclass__(**kwargs) to support multiple inheritance!
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 |
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¶
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¶
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¶
"""
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()