Back to Blog
Python

Python Metaclass vs __init_subclass__: When to Use Each

python metaclass vs **init_subclass**: Compare Python metaclasses and __init_subclass__ to control class creation. Learn when each approach fits, how they differ, and...

PythonMetaclass__init_subclass__Class CreationOOPClass Hooks
Diagram comparing Python metaclass and __init_subclass__ hooks during class creation

In Python, the moment a class is defined triggers a sequence of runtime operations. Two mechanisms let you intercept that moment: a metaclass, which replaces the default type as the class factory, and __init_subclass__, a hook defined on a parent class that runs after a subclass is created. The choice between python metaclass vs **init_subclass** depends on where you want the customization logic to live and how much control you need over the class creation process.

What Happens When a Class Is Created

When you write class Child(Parent):, Python performs several steps. It calls the metaclass of Parent (or type by default) to create the Child class object. The metaclass's __new__ and __init__ methods receive the class name, bases, and namespace. After the class object is constructed, Python checks if any base class defines __init_subclass__. If so, it calls that method with the newly created class as an argument. This happens before the class name is bound in the enclosing scope.

The key distinction is that a metaclass controls the entire creation process, including how the class body is executed and how the class object is built. __init_subclass__ is a callback that runs after the class exists, giving you a chance to inspect or modify it, but not to alter the creation mechanism itself.

Metaclass: The Explicit Class Factory

A metaclass is a class whose instances are classes. By default, type is the metaclass of all classes. You can create a custom metaclass by subclassing type and overriding __new__ or __init__. Here is a minimal example that adds a created_at attribute to every class using the metaclass:

import time class TimestampMeta(type): def __new__(mcls, name, bases, namespace): cls = super().__new__(mcls, name, bases, namespace) cls.created_at = time.time() return cls class Base(metaclass=TimestampMeta): pass class Child(Base): pass print(Child.created_at) # e.g., 1734567890.123

The metaclass runs for every class that uses it, including Base itself. It can modify the namespace before the class is created, add or remove methods, validate attributes, or even change the bases. Because the metaclass is inherited by subclasses, any class derived from Base will also go through TimestampMeta.

init_subclass: The Hook Inside the Parent Class

__init_subclass__ is a classmethod defined on a parent class. It is called automatically when a subclass of that parent is created. You do not need a custom metaclass; the default type invokes it. Here is the equivalent timestamp example using __init_subclass__:

import time class Base: def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) cls.created_at = time.time() class Child(Base): pass print(Child.created_at) # e.g., 1734567890.456

Notice that Base itself does not get a created_at attribute because __init_subclass__ is only called for subclasses, not for the class that defines it. If you need the base class to have the attribute as well, you must set it manually or use a metaclass.

__init_subclass__ receives the new class as the first argument. It can also accept keyword arguments that are passed in the class definition, as long as they are forwarded via **kwargs to super().__init_subclass__.

Key Differences: Timing, Scope, and Control

The most important differences between metaclasses and __init_subclass__ are timing, scope, and level of control.

AspectMetaclassinit_subclass
When it runsDuring class creation, before the class object existsAfter the class object is created, before binding
Access to namespaceFull access to the namespace before class creationOnly sees the finished class object
Can change basesYes, in __new__No
Can modify class attributesYes, before or after creationYes, after creation
Inherited behaviorAutomatically inherited by all subclassesInherited as a method; called for each subclass
Conflict with other metaclassesCan cause metaclass conflictsNo conflict, works with any metaclass
BoilerplateRequires subclassing typeSimple method definition

A metaclass gives you the ability to rewrite the class body, change the bases, or even return a different class object. __init_subclass__ is a post-processing hook: the class is already fully formed, so you can inspect and mutate it, but you cannot change its creation path.

When to Use a Metaclass

Reach for a metaclass when you need to intervene before the class object exists. Typical use cases include:

  • Validating or transforming the class namespace before the class is created.
  • Automatically adding methods or properties based on class attributes.
  • Changing the bases of a class, for example to inject a mixin.
  • Implementing a registry that must capture classes at definition time, but where you also need to control the class creation order.
  • Working with libraries that already use metaclasses and require you to extend them.

For instance, a metaclass that enforces that every subclass defines a run method:

class RunRequiredMeta(type): def __new__(mcls, name, bases, namespace): if name != 'Base' and 'run' not in namespace: raise TypeError(f'{name} must define a run() method') return super().__new__(mcls, name, bases, namespace) class Base(metaclass=RunRequiredMeta): pass class Good(Base): def run(self): pass class Bad(Base): pass # Raises TypeError

Here the metaclass checks the namespace before the class is created, which is impossible with __init_subclass__ because the class already exists by the time the hook runs.

When to Use init_subclass

__init_subclass__ is the lighter-weight option. Use it when you only need to react to the creation of a subclass, not control the creation itself. Common scenarios include:

  • Registering subclasses in a registry.
  • Applying a decorator to every subclass method.
  • Adding a class attribute that depends on the subclass name or other attributes.
  • Implementing a simple plugin system where each subclass is automatically discovered.

Here is a registry example:

class PluginBase: registry = {} def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) PluginBase.registry[cls.__name__] = cls class PluginA(PluginBase): pass class PluginB(PluginBase): pass print(PluginBase.registry) # {'PluginA': <class '__main__.PluginA'>, 'PluginB': <class '__main__.PluginB'>}

Because __init_subclass__ is just a method, it is easier to understand and maintain than a metaclass. It also avoids metaclass conflicts when you need to combine with other libraries that define their own metaclasses.

Combining Both Approaches

You can use a metaclass and __init_subclass__ together. The metaclass controls class creation, and __init_subclass__ runs as a post-creation hook. This is useful when you need both early and late customization. For example, a metaclass might add a method, and __init_subclass__ might register the class in a registry:

class CombinedMeta(type): def __new__(mcls, name, bases, namespace): namespace['added_by_meta'] = True return super().__new__(mcls, name, bases, namespace) class Base(metaclass=CombinedMeta): registry = [] def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) Base.registry.append(cls) class Child(Base): pass print(Child.added_by_meta) # True print(Base.registry) # [<class '__main__.Child'>]

This pattern works because __init_subclass__ is inherited and called even when the class is created by a custom metaclass. The metaclass runs first, then __init_subclass__ is invoked.

Performance and Maintainability Tradeoffs

The performance difference between a metaclass and __init_subclass__ is negligible in most applications. Both add a small overhead at class definition time, not at instance creation or method call time. The real tradeoffs are in maintainability and flexibility.

A metaclass is a more powerful tool, but it also introduces a separate layer of logic that can be harder to trace. When you see class Base(metaclass=SomeMeta), you must look up SomeMeta to understand what happens. __init_subclass__ is defined directly on the base class, so the behavior is more visible and localized.

Metaclasses also have a compatibility constraint: a class can only have one metaclass. If you try to inherit from two classes that have different metaclasses, Python raises a TypeError unless you define a combined metaclass. __init_subclass__ does not have this problem because it is just a method on the parent class.

Common Pitfalls and Compatibility Considerations

One common mistake with __init_subclass__ is forgetting to call super().__init_subclass__(**kwargs). If you do not forward the keyword arguments, you may break subclassing in unexpected ways, especially when multiple base classes define the hook.

Another pitfall is assuming that __init_subclass__ is called for the base class itself. It is not. If you need the base class to receive the same treatment, you must apply the logic manually or use a metaclass.

With metaclasses, a frequent issue is accidentally overriding __new__ without calling super().__new__ correctly, leading to subtle bugs. Also, because the metaclass is inherited, it affects all subclasses, which may be more than you intended. If you only need to customize a single class, a metaclass is overkill.

Compatibility with third-party libraries is another consideration. If a library uses a metaclass, you cannot easily combine it with another metaclass unless you create a merged metaclass. __init_subclass__ works alongside any metaclass, making it a safer choice for framework code that must interoperate.

When you are building a framework or an internal library and you control all the classes, either approach works. The decision should be based on whether you need to modify the class before it exists. If yes, use a metaclass. If you only need to react to the class after creation, prefer __init_subclass__ for its simplicity and lower coupling.

python metaclass vs **init_subclass**: Practical Usage and C | RYUSLOG DEV