2025, Nov 11 07:00

Understanding Python Class Creation: Metaclasses, __init_subclass__, Class Decorators, and Attribute Flow

Learn how Python creates classes: metaclasses, __init_subclass__, and class decorators, the order of hooks and where attributes land on classes vs instances.

Metaclasses, __init_subclass__, and class decorators all influence how a Python class comes to life, but they act at different points in the lifecycle. If you want to reason about which attributes end up on a class versus on its instances, you need a precise picture of the order in which these hooks run.

Example: one class creation pipeline, three moving parts

The snippet below wires a custom metaclass, a class decorator, and __init_subclass__ across a small hierarchy. It prints markers that reveal the exact sequence of events and sets attributes in each stage to show where they ultimately land.

class Orchestrator(type):
    def __new__(meta, cls_name, parents, ns):
        print(f"Orchestrator.__new__ called for {cls_name}")
        ns['meta_attr'] = f"Value from {meta.__name__}"
        return super().__new__(meta, cls_name, parents, ns)

    def __init__(cls, cls_name, parents, ns):
        print(f"Orchestrator.__init__ called for {cls_name}")
        super().__init__(cls_name, parents, ns)
        cls.meta_init_attr = f"Initialized by {cls.__class__.__name__}"


def label_class(klass):
    print(f"Class decorator called for {klass.__name__}")
    klass.decorated_attr = "Decorated!"
    return klass

@label_class
class RootBase(metaclass=Orchestrator):
    def __init_subclass__(subc, **kw):
        print(f"RootBase.__init_subclass__ called for {subc.__name__}")
        super().__init_subclass__(**kw)
        subc.base_subclass_attr = "From MyBase __init_subclass__"

    def __init__(self):
        print("RootBase instance __init__ called")
        self.instance_attr = "Instance created"

print("\nDefining SubType:")
class SubType(RootBase):
    def __init_subclass__(subc, **kw):
        print(f"SubType.__init_subclass__ called for {subc.__name__}")
        super().__init_subclass__(**kw)
        subc.derived_subclass_attr = "From MyDerived __init_subclass__"

    def __init__(self):
        print("SubType instance __init__ called")
        super().__init__()

print("\nCreating SubType instance:")
obj = SubType()

print("\nAttributes on SubType class:")
print(f"SubType.meta_attr: {hasattr(SubType, 'meta_attr')}")
print(f"SubType.meta_init_attr: {hasattr(SubType, 'meta_init_attr')}")
print(f"SubType.decorated_attr: {hasattr(SubType, 'decorated_attr')}")
print(f"SubType.base_subclass_attr: {hasattr(SubType, 'base_subclass_attr')}")
print(f"SubType.derived_subclass_attr: {hasattr(SubType, 'derived_subclass_attr')}")

print("\nAttributes on instance:")
print(f"obj.instance_attr: {hasattr(obj, 'instance_attr')}")

What actually happens during class creation

When Python hits a class statement, it first determines the metaclass, typically from the bases. If the metaclass provides __prepare__, that runs to build the namespace that will collect the class body. Next, Python executes the class body in that namespace, recording methods, attributes, and automatic entries like __module__, __qualname__, and any __annotations__.

With the namespace ready, the metaclass’s __new__ is called. Inside it, the actual class object is created by ultimately delegating to type.__new__. The critical moment is here: when type.__new__ creates the class, any __init_subclass__ defined on superclasses runs. That is the point where base classes can customize newly created subclasses. After __new__ completes, the metaclass’s __init__ runs for final initialization of the class object. Only after the class is fully constructed does Python apply a class decorator, passing the class to the decorator and binding whatever it returns as the class name in the surrounding scope.

Later, when you instantiate the class, the normal __init__ on the class executes and can place instance-specific attributes on the new object.

The fix and the payoff

One detail that matters if you run the metaclass code yourself: in the metaclass __init__, the super call should not pass the class as the first positional argument. The correct call shape is this:

class Orchestrator(type):
    def __init__(cls, cls_name, parents, ns):
        # ...
        super().__init__(cls_name, parents, ns)
        # ...

With that in place, the flow described above unfolds exactly as intended. The metaclass __new__ can inject meta_attr into the class namespace before creation, the metaclass __init__ can set meta_init_attr afterward, and the decorator runs last to set decorated_attr. When SubType is created as a subclass of RootBase, RootBase.__init_subclass__ executes during type.__new__, which sets base_subclass_attr. SubType’s own __init_subclass__ only runs when a subclass of SubType is created, so it does not affect SubType itself. Finally, creating an instance triggers the instance-level __init__, which sets instance_attr on the object.

Which attributes land where

On the derived class, the attributes meta_attr, meta_init_attr, decorated_attr, and base_subclass_attr are present. On the instance, instance_attr is present. The attribute derived_subclass_attr is not present on the derived class or its instance, because the __init_subclass__ that sets it belongs to the derived class and only runs when creating further subclasses of it. Also remember that class attributes are visible from instances via the normal attribute lookup fallback, even though they are stored on the class.

Why this matters

In day-to-day work you rarely need to memorize the entire sequence. The crucial pieces are that class decorators apply last during class creation and that __init__ executes when you create instances. Both class decorators and __init_subclass__ exist to customize class creation without resorting to a custom metaclass, and modern code can often stay within those two mechanisms.

Closing advice

Keep a simple mental model: the metaclass constructs the class, base classes get a chance to tweak new subclasses via __init_subclass__ during type.__new__, the metaclass __init__ performs any final setup, and only then does the class decorator run. If you need to check where an attribute is coming from, map it to the stage that sets it. Use a decorator or __init_subclass__ for most customization needs, and reserve a metaclass for scenarios that truly need it.

The article is based on a question from StackOverflow by user31555720 and an answer by jsbueno.