Skip to content

Metaclasses Introduction

A metaclass is a class whose instances are classes. Just as a class defines how instances behave, a metaclass defines how classes behave.


Understanding the Concept

Everything is an Object

In Python, classes are objects too:

class MyClass:
    pass

# MyClass is an object
print(type(MyClass))  # <class 'type'>

# Just like instances are objects
obj = MyClass()
print(type(obj))      # <class '__main__.MyClass'>

The type() Function

type has two uses:

# 1. Get the type of an object
print(type(42))        # <class 'int'>
print(type("hello"))   # <class 'str'>
print(type([1, 2]))    # <class 'list'>

# 2. Create a new class dynamically
MyClass = type('MyClass', (), {'x': 10})
print(MyClass.x)       # 10

type is the Default Metaclass

class MyClass:
    pass

# These are equivalent:
# 1. Using class statement
class MyClass:
    x = 10

# 2. Using type() directly
MyClass = type('MyClass', (), {'x': 10})

The Class Creation Process

When Python sees a class definition:

class MyClass(BaseClass):
    x = 10
    def method(self):
        pass

It executes roughly:

# 1. Collect class body into a namespace dict
namespace = {'x': 10, 'method': method_function}

# 2. Determine metaclass (default: type)
metaclass = type

# 3. Call metaclass to create class
MyClass = metaclass('MyClass', (BaseClass,), namespace)

Creating a Metaclass

Basic Metaclass

class MyMeta(type):
    def __new__(mcs, name, bases, namespace):
        print(f"Creating class: {name}")
        # Create the class using type.__new__
        cls = super().__new__(mcs, name, bases, namespace)
        return cls

class MyClass(metaclass=MyMeta):
    pass
# Output: Creating class: MyClass

new vs init in Metaclass

class MyMeta(type):
    def __new__(mcs, name, bases, namespace):
        """Called to CREATE the class object."""
        print(f"__new__: Creating {name}")
        return super().__new__(mcs, name, bases, namespace)

    def __init__(cls, name, bases, namespace):
        """Called to INITIALIZE the class object."""
        print(f"__init__: Initializing {name}")
        super().__init__(name, bases, namespace)

class MyClass(metaclass=MyMeta):
    pass
# __new__: Creating MyClass
# __init__: Initializing MyClass

call in Metaclass

Controls instance creation:

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        """Called when MyClass() is invoked."""
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class Singleton(metaclass=SingletonMeta):
    pass

a = Singleton()
b = Singleton()
print(a is b)  # True

Practical Examples

Automatic Registration

class PluginMeta(type):
    plugins = {}

    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        # Don't register the base class itself
        if bases:  # Has parent classes
            mcs.plugins[name] = cls
        return cls

class Plugin(metaclass=PluginMeta):
    """Base class for plugins."""
    pass

class AudioPlugin(Plugin):
    pass

class VideoPlugin(Plugin):
    pass

print(PluginMeta.plugins)
# {'AudioPlugin': <class 'AudioPlugin'>, 'VideoPlugin': <class 'VideoPlugin'>}

Attribute Validation

class ValidatedMeta(type):
    def __new__(mcs, name, bases, namespace):
        # Check that required attributes exist
        if bases:  # Not the base class
            if 'required_field' not in namespace:
                raise TypeError(f"{name} must define 'required_field'")
        return super().__new__(mcs, name, bases, namespace)

class Base(metaclass=ValidatedMeta):
    pass

class Valid(Base):
    required_field = "I exist"

# class Invalid(Base):  # TypeError: Invalid must define 'required_field'
#     pass

Method Wrapping

import functools

class LoggedMeta(type):
    def __new__(mcs, name, bases, namespace):
        # Wrap all methods with logging
        for key, value in namespace.items():
            if callable(value) and not key.startswith('_'):
                namespace[key] = mcs.log_call(value)
        return super().__new__(mcs, name, bases, namespace)

    @staticmethod
    def log_call(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"Calling {func.__name__}")
            return func(*args, **kwargs)
        return wrapper

class MyClass(metaclass=LoggedMeta):
    def method1(self):
        return "result1"

    def method2(self, x):
        return x * 2

obj = MyClass()
obj.method1()  # Calling method1
obj.method2(5) # Calling method2

Interface Enforcement

class InterfaceMeta(type):
    required_methods = []

    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)

        # Skip check for base class
        if bases and mcs.required_methods:
            for method in mcs.required_methods:
                if method not in namespace:
                    raise TypeError(
                        f"{name} must implement {method}()"
                    )
        return cls

class ServiceMeta(InterfaceMeta):
    required_methods = ['start', 'stop', 'status']

class Service(metaclass=ServiceMeta):
    """Base service class."""
    def start(self): pass
    def stop(self): pass
    def status(self): pass

class DatabaseService(Service):
    def start(self):
        print("DB starting")

    def stop(self):
        print("DB stopping")

    def status(self):
        return "running"

Metaclass vs Alternatives

When NOT to Use Metaclasses

Most use cases have simpler alternatives:

Need Alternative
Modify class creation __init_subclass__
Add methods Class decorators
Validate attributes Descriptors
Singleton pattern Module-level instance
Registration Class decorators

init_subclass (Python 3.6+)

Simpler alternative for many cases:

class Plugin:
    plugins = {}

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        Plugin.plugins[cls.__name__] = cls

class AudioPlugin(Plugin):
    pass

class VideoPlugin(Plugin):
    pass

print(Plugin.plugins)
# {'AudioPlugin': <class 'AudioPlugin'>, 'VideoPlugin': <class 'VideoPlugin'>}

Class Decorators

def register(cls):
    registry[cls.__name__] = cls
    return cls

def add_method(cls):
    cls.new_method = lambda self: "added"
    return cls

@register
@add_method
class MyClass:
    pass

Advanced: prepare

Control the namespace dict used during class creation:

from collections import OrderedDict

class OrderedMeta(type):
    @classmethod
    def __prepare__(mcs, name, bases):
        # Return custom namespace (must be dict-like)
        return OrderedDict()

    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, dict(namespace))
        cls._field_order = list(namespace.keys())
        return cls

class Record(metaclass=OrderedMeta):
    first = 1
    second = 2
    third = 3

print(Record._field_order)
# ['__module__', '__qualname__', 'first', 'second', 'third']

When to Use Metaclasses

Use metaclasses when you need to:

  • Modify class creation before the class exists
  • Automatically register all subclasses
  • Enforce class-level invariants
  • Create DSLs (Domain Specific Languages)
  • Build frameworks (ORMs, serializers)

Famous examples:

  • Django models (ModelBase)
  • SQLAlchemy ORM (DeclarativeMeta)
  • ABC module (ABCMeta)
  • Enum (EnumMeta)

Summary

# Class hierarchy
object  # Base of all objects
   
 type   # Metaclass - creates classes (type is its own metaclass)
   
MyMeta  # Custom metaclass
   
MyClass # Regular class (instance of MyMeta)
   
  obj   # Instance of MyClass
Hook Called When Use For
__new__ Creating class Modify namespace, validate
__init__ Initializing class Post-creation setup
__call__ Creating instance Singleton, pooling
__prepare__ Before body execution Custom namespace

Key Takeaways:

  • Classes are instances of metaclasses
  • type is the default metaclass for all classes
  • Metaclasses control class creation and behavior
  • Use __init_subclass__ or decorators when possible
  • Reserve metaclasses for framework-level code
  • "If you wonder whether you need metaclasses, you don't" — Tim Peters

Runnable Example: class_factory_type.py

"""
TUTORIAL: Class Factory Using type() - Creating Classes Dynamically
===================================================================

In this tutorial, you'll learn how to create classes dynamically using the
type() builtin. Normally, type(obj) returns the type of an object, but type()
has another mode: creating new classes!

Normal usage: type(obj) → returns the class

Dynamic creation: type(name, bases, dict) → creates a NEW class

The three arguments to type():
  1. name (str): The name of the new class
  2. bases (tuple): Parent classes (can be empty for object)
  3. dict (dict): Class attributes and methods

Why create classes dynamically?
  - Build flexible frameworks where classes are generated at runtime
  - Reduce boilerplate when creating many similar classes
  - Generate specialized classes based on data or configuration
  - Create classes from strings (parsing, serialization)

In this example:
  - record_factory() creates classes dynamically using type()
  - These classes are tuples with named fields and custom methods
  - They demonstrate the full power of runtime class creation

Key insight: Classes are objects too! You can create them, modify them,
and pass them around just like any other Python object.
"""

from typing import Union, Any
from collections.abc import Iterable, Iterator


# ============ Example 1: Understanding type() for Class Creation ============

if __name__ == "__main__":
    print("=" * 70)
    print("EXAMPLE 1: Creating classes with type()")
    print("=" * 70)

    # The type() builtin has two modes:
    print(f"\nMode 1: type(obj) returns the class of obj")
    print(f"  type(42) = {type(42)}")
    print(f"  type('hello') = {type('hello')}")
    print(f"  type([1, 2, 3]) = {type([1, 2, 3])}")

    print(f"\nMode 2: type(name, bases, dict) creates a NEW class")
    print(f"  MyClass = type('MyClass', (), {{}})")
    print(f"  Creates: a class named MyClass with no parents and no attributes")

    # Create a simple class dynamically
    MyClass = type('MyClass', (), {})
    print(f"\nMyClass = {MyClass}")
    print(f"  Type: {type(MyClass)}")
    print(f"  Instance: {MyClass()}")


    # ============ Example 2: Adding Attributes and Methods ============
    print("\n" + "=" * 70)
    print("EXAMPLE 2: Adding methods to dynamically created classes")
    print("=" * 70)

    def say_hello(self):
        """A method to add to the class."""
        return f'Hello from {self.__class__.__name__}'

    def __init__(self, name):
        """An initializer for the class."""
        self.name = name

    def __repr__(self):
        """String representation."""
        return f'{self.__class__.__name__}(name={self.name!r})'

    # Create a class with methods
    Greeter = type('Greeter',
                   (),
                   {'__init__': __init__,
                    'say_hello': say_hello,
                    '__repr__': __repr__,
                    'class_var': 'I am a class variable'})

    print(f"\nGreeter = type('Greeter', (), {{'__init__': ..., 'say_hello': ..., ...}})")
    print(f"\nCreated class: {Greeter}")
    print(f"Class variable: {Greeter.class_var}")

    greeter = Greeter('Alice')
    print(f"\ngreeter = Greeter('Alice')")
    print(f"  greeter.name = '{greeter.name}'")
    print(f"  repr(greeter) = {repr(greeter)}")
    print(f"  greeter.say_hello() = '{greeter.say_hello()}'")


    # ============ Example 3: The record_factory() Function ============
    print("\n" + "=" * 70)
    print("EXAMPLE 3: Practical example - record_factory()")
    print("=" * 70)

    FieldNames = Union[str, Iterable[str]]

    def record_factory(cls_name: str, field_names: FieldNames) -> type[tuple]:
        """Create a class for holding data, like a namedtuple.

        This factory function dynamically creates classes that:
        1. Store data efficiently using __slots__
        2. Support unpacking (for x, y in records)
        3. Have a nice __repr__ for debugging
        4. Accept both positional and keyword arguments

        Args:
            cls_name: The name for the new class.
            field_names: Field names as a string ('name age') or iterable.

        Returns:
            A new class that can be instantiated with the given fields.
        """

        slots = parse_identifiers(field_names)

        def __init__(self, *args, **kwargs) -> None:
            """Initialize instance, accepting positional and keyword args."""
            attrs = dict(zip(self.__slots__, args))
            attrs.update(kwargs)
            for name, value in attrs.items():
                setattr(self, name, value)

        def __iter__(self) -> Iterator[Any]:
            """Allow unpacking of the instance (for x, y in instances)."""
            for name in self.__slots__:
                yield getattr(self, name)

        def __repr__(self):
            """Return a nice representation like: Dog(name='Rex', weight=30)."""
            values = ', '.join(f'{name}={value!r}'
                for name, value in zip(self.__slots__, self))
            cls_name_local = self.__class__.__name__
            return f'{cls_name_local}({values})'

        # Prepare the class attributes dictionary
        cls_attrs = dict(
            __slots__=slots,
            __init__=__init__,
            __iter__=__iter__,
            __repr__=__repr__,
        )

        # Create and return the class using type()
        return type(cls_name, (object,), cls_attrs)


    def parse_identifiers(names: FieldNames) -> tuple[str, ...]:
        """Parse field names from string or iterable.

        Args:
            names: Either a string like 'name age weight' or a list/tuple.

        Returns:
            Tuple of validated identifier strings.

        Raises:
            ValueError: If any name is not a valid Python identifier.
        """
        if isinstance(names, str):
            names = names.replace(',', ' ').split()
        if not all(s.isidentifier() for s in names):
            raise ValueError('names must all be valid identifiers')
        return tuple(names)


    print(f"\nrecord_factory() creates data classes using type()")
    print(f"\nSignature: record_factory(cls_name, field_names) -> class")
    print(f"\nExample: Dog = record_factory('Dog', 'name weight owner')")


    # ============ Example 4: Using the Created Classes ============
    print("\n" + "=" * 70)
    print("EXAMPLE 4: Creating and using classes from record_factory()")
    print("=" * 70)

    # Create a Dog class
    Dog = record_factory('Dog', 'name weight owner')

    print(f"\nDog = record_factory('Dog', 'name weight owner')")
    print(f"  Created: {Dog}")
    print(f"  Fields (__slots__): {Dog.__slots__}")

    # Create instances
    rex = Dog('Rex', 30, 'Bob')
    buddy = Dog('Buddy', 25, 'Alice')

    print(f"\nrex = Dog('Rex', 30, 'Bob')")
    print(f"  repr(rex) = {repr(rex)}")

    print(f"\nbuddy = Dog('Buddy', 25, 'Alice')")
    print(f"  repr(buddy) = {repr(buddy)}")

    print(f"\nAccessing attributes:")
    print(f"  rex.name = '{rex.name}'")
    print(f"  rex.weight = {rex.weight}")
    print(f"  rex.owner = '{rex.owner}'")


    # ============ Example 5: Unpacking and Iteration ============
    print("\n" + "=" * 70)
    print("EXAMPLE 5: Unpacking and iteration support")
    print("=" * 70)

    print(f"\nUnpacking (like tuple unpacking):")
    name, weight, owner = rex
    print(f"  name, weight, owner = rex")
    print(f"  name = '{name}', weight = {weight}, owner = '{owner}'")

    print(f"\nIndexing (like tuple indexing):")
    print(f"  rex[0] = '{rex[0]}'  (name)")
    print(f"  rex[1] = {rex[1]}  (weight)")
    print(f"  rex[2] = '{rex[2]}'  (owner)")

    print(f"\nFormatting with unpacking:")
    message = "{2}'s dog {0} weighs {1}kg"
    print(f"  message = \"{message}\"")
    print(f"  message.format(*rex) = '{message.format(*rex)}'")

    print(f"\nIteration:")
    print(f"  for value in rex:")
    for i, value in enumerate(rex):
        print(f"    [{i}] = {value!r}")


    # ============ Example 6: Modifying Instance Attributes ============
    print("\n" + "=" * 70)
    print("EXAMPLE 6: Instances are mutable (can be modified)")
    print("=" * 70)

    print(f"\nOriginal: rex = {repr(rex)}")

    rex.weight = 32
    print(f"  rex.weight = 32")
    print(f"  Modified: rex = {repr(rex)}")

    print(f"\nThis is different from namedtuples (immutable)")
    print(f"  - record_factory classes are mutable")
    print(f"  - namedtuples are immutable")
    print(f"  - Both are efficient with __slots__")


    # ============ Example 7: Creating with String or List ============
    print("\n" + "=" * 70)
    print("EXAMPLE 7: Flexible field name input")
    print("=" * 70)

    # Create with string
    Person = record_factory('Person', 'name age city')
    print(f"\nPerson = record_factory('Person', 'name age city')")
    print(f"  Fields: {Person.__slots__}")

    alice = Person('Alice', 30, 'NYC')
    print(f"  alice = Person('Alice', 30, 'NYC')")
    print(f"  repr(alice) = {repr(alice)}")

    # Create with list
    Product = record_factory('Product', ['id', 'name', 'price'])
    print(f"\nProduct = record_factory('Product', ['id', 'name', 'price'])")
    print(f"  Fields: {Product.__slots__}")

    item = Product(1, 'Laptop', 999.99)
    print(f"  item = Product(1, 'Laptop', 999.99)")
    print(f"  repr(item) = {repr(item)}")

    # Create with comma-separated string
    Coordinate = record_factory('Coordinate', 'x,y,z')
    print(f"\nCoordinate = record_factory('Coordinate', 'x,y,z')")
    print(f"  Fields: {Coordinate.__slots__}")

    point = Coordinate(10, 20, 30)
    print(f"  point = Coordinate(10, 20, 30)")
    print(f"  repr(point) = {repr(point)}")


    # ============ Example 8: Keyword Arguments ============
    print("\n" + "=" * 70)
    print("EXAMPLE 8: Creating instances with keyword arguments")
    print("=" * 70)

    # Using positional arguments
    dog1 = Dog('Max', 28, 'Charlie')
    print(f"\ndog1 = Dog('Max', 28, 'Charlie')")
    print(f"  {repr(dog1)}")

    # Using keyword arguments
    dog2 = Dog(name='Lucy', weight=24, owner='Diana')
    print(f"\ndog2 = Dog(name='Lucy', weight=24, owner='Diana')")
    print(f"  {repr(dog2)}")

    # Using mixed positional and keyword
    dog3 = Dog('Buddy', owner='Eve', weight=26)
    print(f"\ndog3 = Dog('Buddy', owner='Eve', weight=26)")
    print(f"  {repr(dog3)}")


    # ============ Example 9: Why Use type() for Class Creation ============
    print("\n" + "=" * 70)
    print("EXAMPLE 9: Why dynamically create classes with type()")
    print("=" * 70)

    print(f"\nBenefits of type()-based class creation:")
    print(f"  1. Reduce boilerplate - don't repeat similar classes")
    print(f"  2. Generate from data - create classes from strings, configs")
    print(f"  3. Framework design - base frameworks on runtime class generation")
    print(f"  4. Meta-programming - inspect and modify classes at runtime")

    print(f"\nUse cases:")
    print(f"  - ORM systems (database mapping): create model classes dynamically")
    print(f"  - Data validation: generate validators from schemas")
    print(f"  - API clients: generate resource classes from API specs")
    print(f"  - Configuration: create classes based on config files")

    print(f"\nAlternatives to type():")
    print(f"  - dataclass: for data-holding classes with less boilerplate")
    print(f"  - namedtuple: for immutable tuple-like classes")
    print(f"  - type(): for maximum flexibility and runtime creation")


    # ============ Example 10: Class Hierarchy ============
    print("\n" + "=" * 70)
    print("EXAMPLE 10: Classes created with type() in the hierarchy")
    print("=" * 70)

    print(f"\nDog.__mro__ (method resolution order):")
    print(f"  {Dog.__mro__}")

    print(f"\nDog class details:")
    print(f"  Dog.__name__ = '{Dog.__name__}'")
    print(f"  Dog.__bases__ = {Dog.__bases__}")
    print(f"  Dog.__dict__ keys = {list(Dog.__dict__.keys())}")

    print(f"\nInstance details:")
    print(f"  rex.__class__.__name__ = '{rex.__class__.__name__}'")
    print(f"  isinstance(rex, Dog) = {isinstance(rex, Dog)}")
    print(f"  type(rex) = {type(rex)}")
    print(f"  type(type(rex)) = {type(type(rex))}")

    print(f"\nThe full chain:")
    print(f"  rex is an instance of Dog")
    print(f"  Dog is an instance of type (it's a class)")
    print(f"  type is an instance of type (it's a metaclass)")


    # ============ Example 11: Summary - Dynamic Class Creation ============
    print("\n" + "=" * 70)
    print("EXAMPLE 11: Summary - When and How to Use type()")
    print("=" * 70)

    print(f"\nThe type() function has three jobs:")
    print(f"  1. type(obj) - returns the type of an object")
    print(f"  2. type(name, bases, dict) - creates a new class")
    print(f"  3. type as a metaclass - controls class creation")

    print(f"\nCreating classes with type():")
    print(f"  Syntax: NewClass = type(name, bases, attributes_dict)")
    print(f"  name: String name for the class")
    print(f"  bases: Tuple of parent classes (or () for object)")
    print(f"  attributes_dict: Dict of methods and class variables")

    print(f"\nKey benefits:")
    print(f"  - Powerful for framework and library design")
    print(f"  - Can generate many similar classes from data")
    print(f"  - Allows runtime inspection and modification")

    print(f"\nImportant notes:")
    print(f"  - Classes created with type() are normal classes")
    print(f"  - They work with inheritance, isinstance(), etc.")
    print(f"  - __slots__ makes them memory-efficient")
    print(f"  - Custom methods work just like normal classes")

    print(f"\n" + "=" * 70)