Skip to content

__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()