Create your own
Lesson illustration

Class Creation: Namespace Preparation, Metaclasses, and Class-Body Execution

Good to see you again. In the previous lesson, you used __init_subclass__ to validate and register job-handler subclasses at definition time. That hook acts on a class after Python has constructed it. To understand when a metaclass is warranted, you now need to look one layer earlier: how Python turns a class statement into the class object that __init_subclass__ receives.

This lesson traces that construction process: selecting a metaclass, preparing the namespace in which the body runs, executing the body, and finally creating the class object. The goal is not to treat metaclasses as mysterious machinery, but to make a class definition something you can inspect and reason about when building framework-style infrastructure.


A class statement executes code

A class definition is not merely a declaration for the interpreter to remember. It is executable runtime work.

For a simple class:

class RetryPolicy:
    default_timeout = 60

    def timeout_for_attempt(self, attempt: int) -> int:
        return self.default_timeout * attempt

Python does not just store source code. It:

  1. Determines what will construct the class object.
  2. Creates a temporary namespace for the class body.
  3. Executes default_timeout = 60 and creates the function object for timeout_for_attempt.
  4. Uses the resulting namespace to construct the RetryPolicy class object.
  5. Binds that finished object to the name RetryPolicy.

In the ordinary case, the constructor is type. This equivalence is a useful approximation:

RetryPolicy = type(
    "RetryPolicy",
    (),
    {
        "default_timeout": 60,
        "timeout_for_attempt": timeout_for_attempt,
    },
)

But it is only an approximation. Normal class syntax also compiles and executes the body, creates special attributes such as __module__ and __qualname__, supports zero-argument super(), invokes descriptor and subclass hooks, and applies class decorators.

Watch the short conceptual introduction below before working through the more precise trace.

Metaclasses in Python

Watch “Metaclasses in Python” by mCoding for a compact mental model: classes are objects, type normally creates them, and a metaclass replaces or extends that construction role.

Watch classes as objects to connect a normal class definition to class construction at runtime. Continue with custom metaclasses to see where __prepare__, __new__, and __init__ fit. Focus on the distinction between creating a class object and creating an instance of that class.

A useful level distinction is:

Object being createdUsually created byRelevant hook
An instance, such as SendEmailHandler()The class object__new__, __init__ on SendEmailHandler
A class, such as class SendEmailHandler: ...The metaclass, usually type__prepare__, metaclass __new__, metaclass __init__

A metaclass is therefore the type of a class object:

class JobHandler:
    pass

assert type(JobHandler) is type
assert type(JobHandler()) is JobHandler

If JobHandler were created by HandlerMeta, then type(JobHandler) is HandlerMeta.


The language-level sequence

The official language reference gives the reliable order. Read it now, then use the rest of the lesson to turn it into an observable experiment.

3. Data model — Python 3.14.2 documentation

Read the Python Language Reference’s “Customizing class creation” sections. It is the authoritative account of the class-construction phases that metaclasses customize.

In Section 3.3.3.1, begin with the five-step overview. Treat it as the backbone for this lesson. Then read Section 3.3.3.3, “Determining the appropriate metaclass,” especially metaclass compatibility. Focus on why Python needs one metaclass compatible with every base class. In Section 3.3.3.4, “Preparing the class namespace,” read namespace preparation. Notice that __prepare__ supplies the mapping in which the body executes. Next read Section 3.3.3.5, “Executing the class body,” from the execution explanation. Pay special attention to the fact that a class body executes approximately like exec with distinct global and local namespaces. Finally, in Section 3.3.3.6, “Creating the class object,” read from metaclass invocation, followed by the final binding step. Note the relative order of __set_name__, __init_subclass__, and class decorators.

For typical application code, the full process can be understood as eight stages:

  1. Python evaluates the bases in the class header and resolves unusual non-class bases that use __mro_entries__.
  2. Python chooses a compatible metaclass.
  3. Python asks that metaclass for a namespace via __prepare__, if provided.
  4. Python executes the class body in that namespace.
  5. Python calls the metaclass with the class name, resolved bases, populated namespace, and class-header keywords.
  6. The metaclass creates and initializes the class object.
  7. During normal type-based construction, descriptors receive __set_name__, then the immediate parent receives __init_subclass__.
  8. Python applies any class decorators and binds the final result to the class name in the surrounding scope.

The last two steps explain the previous lesson precisely: your JobHandler.__init_subclass__ hook runs only after Python has a newly created subclass.


Selecting the metaclass

Python must choose exactly one object that will construct the new class. Usually that object is type, but base classes can bring their own metaclasses.

The normal case

class JobHandler:
    pass

class SendEmailHandler(JobHandler):
    pass

assert type(JobHandler) is type
assert type(SendEmailHandler) is type

Because no custom metaclass appears in the header or in a base class, Python uses type.

Inheriting a metaclass

class HandlerMeta(type):
    pass


class JobHandler(metaclass=HandlerMeta):
    pass


class SendEmailHandler(JobHandler):
    pass


assert type(JobHandler) is HandlerMeta
assert type(SendEmailHandler) is HandlerMeta

SendEmailHandler does not need to repeat metaclass=HandlerMeta. Its base class already requires that metaclass, so the subclass is created with it too.

The “most derived” compatible metaclass

If a class header has an explicit metaclass and one or more bases, Python compares all candidates:

  • the explicitly requested metaclass, if present;
  • type(base) for every base class.

The selected metaclass must be a subclass of every candidate metaclass.

class HandlerMeta(type):
    pass


class InstrumentedHandlerMeta(HandlerMeta):
    pass


class JobHandler(metaclass=HandlerMeta):
    pass


class AuditedHandler(
    JobHandler,
    metaclass=InstrumentedHandlerMeta,
):
    pass


assert type(AuditedHandler) is InstrumentedHandlerMeta

This works because InstrumentedHandlerMeta is a subclass of HandlerMeta, so it satisfies the requirements of JobHandler.

An unrelated metaclass creates a conflict:

class UnrelatedMeta(type):
    pass


# This fails:
#
# class BrokenHandler(
#     JobHandler,
#     metaclass=UnrelatedMeta,
# ):
#     pass

Python raises TypeError because neither HandlerMeta nor UnrelatedMeta is a subclass of the other. This is one reason metaclasses should be introduced sparingly in reusable libraries: they constrain every future subclass and can make multiple inheritance harder.

A rare language-level edge case exists: if an explicit metaclass argument is callable but is not itself an instance of type, Python uses it directly. Production Python code almost always uses a subclass of type, which is the model to retain here.


Namespace preparation and class-body execution

Once Python has selected the metaclass, it prepares the namespace that will collect the class body’s definitions.

Without a custom __prepare__, Python uses an ordered mapping. In modern Python, an ordinary dict preserves insertion order, so this often looks unremarkable. The important fact is that the class body writes into a dedicated namespace, not directly into the final class object.

A metaclass can control that namespace:

class HandlerMeta(type):
    @classmethod
    def __prepare__(
        mcls,
        name: str,
        bases: tuple[type, ...],
        **options: object,
    ) -> dict[str, object]:
        return {}

Python conceptually does this:

namespace = HandlerMeta.__prepare__(
    "SendEmailHandler",
    (JobHandler,),
)

Then it executes the body with the module globals available for lookup and namespace acting as the class-local mapping.

At top level inside the body, earlier class assignments are visible to later statements:

class RetryPolicy:
    base_seconds = 10
    max_seconds = base_seconds * 6

Thus, RetryPolicy.max_seconds is 60.

But a method does not close over the class namespace:

class RetryPolicy:
    base_seconds = 10

    def initial_delay(self) -> int:
        return base_seconds  # Looks for a local or global name, not this class attribute.

Unless the module happens to define base_seconds, calling initial_delay() raises NameError. Use the instance, the class, or the compiler-provided __class__ reference instead:

class RetryPolicy:
    base_seconds = 10

    def initial_delay(self) -> int:
        return self.base_seconds

    @classmethod
    def maximum_delay(cls) -> int:
        return cls.base_seconds * 6

This behavior follows naturally from the construction model: the class body is an execution environment used to assemble a class namespace. A function defined in that body keeps normal lexical access to enclosing function and module scopes, but it does not treat the temporary class namespace as an enclosing function scope.

__prepare__ becomes useful when the way names are recorded matters. Examples include preserving specialized declaration metadata, rejecting duplicate declarations, or collecting field definitions in a framework. Ordinary configuration validation and subclass registration, as in the previous lesson, usually need only __init_subclass__.


Trace a class being created

The following diagram shows the conceptual nesting: class creation invokes the metaclass, while the metaclass is itself an object governed by its own type. For everyday metaclass work, focus on the lower layer: __prepare__, __new__, and __init__.

A diagram of Python class creation showing the class statement entering a metaclass’s `__prepare__`, `__new__`, and `__init__` stages, while the metaclass call is itself governed by the metaclass’s own type (a meta-metaclass).

Now make the hidden steps visible. Put this in a scratch module such as experiments/class_creation_trace.py and run it as a script.

events: list[str] = []


class RecordingNamespace(dict[str, object]):
    def __setitem__(self, key: str, value: object) -> None:
        events.append(f"body writes {key!r}")
        super().__setitem__(key, value)


class TraceMeta(type):
    @classmethod
    def __prepare__(
        mcls,
        name: str,
        bases: tuple[type, ...],
        **options: object,
    ) -> RecordingNamespace:
        base_names = [base.__name__ for base in bases]
        events.append(
            f"prepare {name!r}, bases={base_names}, options={options}"
        )
        return RecordingNamespace()

    def __new__(
        mcls,
        name: str,
        bases: tuple[type, ...],
        namespace: dict[str, object],
        **options: object,
    ) -> type:
        events.append(
            f"metaclass new {name!r}, keys={list(namespace)}"
        )

        # Forward class-header options so type.__new__ can ultimately
        # deliver them to __init_subclass__.
        return super().__new__(
            mcls,
            name,
            bases,
            namespace,
            **options,
        )

    def __init__(
        cls,
        name: str,
        bases: tuple[type, ...],
        namespace: dict[str, object],
        **options: object,
    ) -> None:
        events.append(f"metaclass init {name!r}")

        # type.__init__ does not need the class-header configuration here.
        super().__init__(name, bases, namespace)


class RecordedField:
    def __set_name__(self, owner: type, name: str) -> None:
        events.append(f"descriptor set_name {owner.__name__}.{name}")


class HandlerBase(metaclass=TraceMeta):
    def run(self) -> str:
        return "base result"

    def __init_subclass__(
        cls,
        /,
        *,
        queue: str,
        **options: object,
    ) -> None:
        events.append(
            f"parent init_subclass {cls.__name__}, queue={queue!r}"
        )
        super().__init_subclass__(**options)
        cls.queue = queue


events.clear()


class SendEmailHandler(HandlerBase, queue="critical"):
    label = "send_email"
    timeout_seconds = 30
    retry_count = timeout_seconds // 10

    endpoint = RecordedField()

    print("executing SendEmailHandler body")

    def run(self) -> str:
        return super().run()


for event in events:
    print(event)

The exact formatting of the key list can vary slightly by Python version, but the meaningful order should be close to this:

prepare 'SendEmailHandler', bases=['HandlerBase'], options={'queue': 'critical'}
body writes '__module__'
body writes '__qualname__'
body writes 'label'
body writes 'timeout_seconds'
body writes 'retry_count'
body writes 'endpoint'
executing SendEmailHandler body
body writes 'run'
body writes '__classcell__'
metaclass new 'SendEmailHandler', keys=[...]
descriptor set_name SendEmailHandler.endpoint
parent init_subclass SendEmailHandler, queue='critical'
metaclass init 'SendEmailHandler'

Here is what each part proves.

__prepare__ happens before the body

TraceMeta.__prepare__ receives:

  • the intended class name, "SendEmailHandler";
  • the resolved base tuple, (HandlerBase,);
  • class-header options such as queue="critical".

It returns RecordingNamespace, so each subsequent assignment made during the body is observable.

The body runs immediately

This line:

print("executing SendEmailHandler body")

runs while the class statement is being evaluated, not when an instance is created.

Similarly, retry_count is calculated while the body runs:

retry_count = timeout_seconds // 10

At that moment, timeout_seconds already exists in the temporary class namespace.

The run method is merely created during this stage. Its body does not execute until someone calls:

SendEmailHandler().run()

The namespace is passed to the metaclass

After the body completes, Python invokes the selected metaclass approximately like this:

SendEmailHandler = TraceMeta(
    "SendEmailHandler",
    (HandlerBase,),
    namespace,
    queue="critical",
)

Calling TraceMeta(...) creates the class object, not a SendEmailHandler instance. TraceMeta.__new__ receives the populated namespace, including the label, timeout_seconds, endpoint, and run entries.

Because run uses zero-argument super(), CPython also places __classcell__ in the namespace. You do not normally manipulate this entry. Its role is to let Python populate the implicit __class__ reference required by zero-argument super().

A metaclass that overrides __new__ should preserve the received namespace and call super().__new__. Dropping __classcell__ can break zero-argument super().

type.__new__ invokes post-creation hooks

TraceMeta.__new__ delegates to type.__new__. That call produces the actual SendEmailHandler object. As part of normal type-based class construction:

  1. RecordedField.__set_name__(SendEmailHandler, "endpoint") runs.
  2. HandlerBase.__init_subclass__ runs with the queue option.
  3. The metaclass’s __init__ initializes the created class object.

That order matters for future descriptor work: descriptors have their owner and assigned name before the parent class’s __init_subclass__ hook runs.

Finally, after creation and hooks succeed, Python would apply any class decorators and then bind the finished object to SendEmailHandler in the surrounding module namespace.


A practical debugging checklist

When class-definition behavior surprises you, determine which phase contains the issue rather than adding prints at random.

SymptomFirst place to inspect
A class keyword is rejected or missingMetaclass selection, __prepare__, metaclass __new__, and __init_subclass__ forwarding
A class-body expression runs during importClass-body execution
A method cannot find a class attribute by bare nameClass-body versus method scoping
A descriptor lacks its attribute name__set_name__ and whether assignment occurred during class construction
A subclass hook does not receive a configuration keywordWhether metaclass __new__ forwarded the keyword to type.__new__
Multiple bases cause a metaclass conflictMetaclass compatibility among all bases
super() fails after custom class creationPreservation of __classcell__ and delegation to type.__new__

For the trace script, keep a few direct assertions immediately after the class definition:

assert type(SendEmailHandler) is TraceMeta
assert SendEmailHandler.queue == "critical"
assert SendEmailHandler.retry_count == 3
assert SendEmailHandler().run() == "base result"

Then temporarily remove **options from TraceMeta.__new__, or stop forwarding them to super().__new__. The resulting failure illustrates a subtle but central rule: class-header options are available to the metaclass during construction, but they reach __init_subclass__ only if the metaclass cooperates with type’s construction process.

Do not retain tracing metaclasses in the capstone itself. Their value is diagnostic and educational. In application code, prefer __init_subclass__ for subclass configuration unless you genuinely need to influence namespace preparation or the construction of the class object.


Takeaways

  • A class statement is executable runtime machinery, not a passive declaration.
  • Python selects a metaclass before running the class body; the selected metaclass must be compatible with every base class’s metaclass.
  • __prepare__ supplies the mapping used as the class-body namespace.
  • Top-level class-body statements execute immediately and populate that namespace; method bodies do not inherit class scope as a lexical closure.
  • Python passes the populated namespace to the metaclass to create the class object.
  • During ordinary type-based creation, descriptor __set_name__ hooks run before the immediate parent’s __init_subclass__.
  • Class decorators run only after the class object has been created and initialized.

Next, you will use this lifecycle deliberately: you will implement a metaclass that validates a class-level contract at class-definition time.

Can't find a good explanation? Sign up and we'll make it for you

Sign up