The Secret Life of Python: Metaclasses - Classes That Make Classes

Published: (December 5, 2025 at 10:15 PM EST)
4 min read
Source: Dev.to

Source: Dev.to

Introduction

Classes are objects. Their type is their metaclass. And type is the ultimate metaclass—it makes classes, including itself.

Timothy had been working with Python classes for years. He understood inheritance, methods, attributes, and the MRO. One afternoon, while debugging, he typed something that broke his mental model of Python entirely.

class Dog:
    def __init__(self, name):
        self.name = name

    def bark(self):
        return f"{self.name} says woof!"

# Create an instance
fido = Dog("Fido")

# Check the type
print(type(fido))  # 

# So far so good. But then...
print(type(Dog))   # 

Timothy stared at the output. “Type of Dog is… type? What does that even mean?”

print(type(type))  # 

“Type of type is type?!” Timothy’s voice rose in confusion.

Margaret, noticing his puzzlement, explained:

“You’ve discovered that classes are objects too. And like all objects, they have a type. The type that makes classes is called a metaclass.”

The conversation continued, revealing that type is its own metaclass—the class that makes classes, including itself.


Everything Is an Object

Inspecting Instances and Classes

class Dog:
    species = "Canis familiaris"

    def __init__(self, name):
        self.name = name

    def bark(self):
        return f"{self.name} says woof!"

# Instance
fido = Dog("Fido")
print(type(fido))           # 
print(isinstance(fido, Dog)) # True
print(fido.__class__)       # 

What is Dog?

# Classes are objects
print(type(Dog))      # 
print(Dog.__class__)  # 

Because classes are first‑class objects, they can be assigned, passed around, and stored just like any other value:

# Assign to another name
MyDog = Dog
rover = MyDog("Rover")
print(rover.bark())   # Rover says woof!

# Pass as argument
def create_instance(cls, *args):
    return cls(*args)

buddy = create_instance(Dog, "Buddy")
print(buddy.bark())   # Buddy says woof!

# Store in a collection
animal_classes = [Dog]
pet = animal_classes[0]("Max")
print(pet.bark())     # Max says woof!

Since classes are objects, they have a type—their metaclass.


The type Metaclass

type as the Default Metaclass

When you write a class definition, Python uses type to create the class object.

class Dog:
    def bark(self):
        return "Woof!"

# Roughly equivalent to:
Dog = type('Dog', (), {'bark': lambda self: "Woof!"})

# Verify
fido = Dog()
print(fido.bark())  # Woof!
print(type(Dog))    # 

Using type() Directly

type() can be called in two ways:

One argument – returns the type of an object:

print(type(42))        # 
print(type("hello"))   # 
print(type([1, 2, 3])) # 

Three arguments – creates a new class:

# type(name, bases, dict)
Dog = type(
    'Dog',                     # class name
    (),                        # base classes (empty tuple → inherits from object)
    {
        'species': 'Canis familiaris',
        'bark': lambda self: "Woof!"
    }
)

print(Dog.species)  # Canis familiaris
fido = Dog()
print(fido.bark())  # Woof!

A more elaborate example:

def __init__(self, name, age):
    self.name = name
    self.age = age

def bark(self):
    return f"{self.name} (age {self.age}) says woof!"

def birthday(self):
    self.age += 1
    return f"{self.name} is now {self.age} years old!"

Dog = type(
    'Dog',
    (object,),
    {
        '__init__': __init__,
        'bark': bark,
        'birthday': birthday,
        'species': 'Canis familiaris'
    }
)

fido = Dog("Fido", 3)
print(fido.bark())      # Fido (age 3) says woof!
print(fido.birthday())  # Fido is now 4 years old!
print(fido.bark())      # Fido (age 4) says woof!

Thus, the class statement is syntactic sugar for a call to the metaclass (normally type).


Creating a Custom Metaclass

You can define your own metaclass by subclassing type.

class Meta(type):
    """A simple custom metaclass"""

    def __new__(mcs, name, bases, namespace):
        print(f"Creating class {name}")
        print(f"  Bases: {bases}")
        print(f"  Attributes: {list(namespace.keys())}")

        # Actually create the class
        cls = super().__new__(mcs, name, bases, namespace)
        return cls

class Dog(metaclass=Meta):
    def bark(self):
        return "Woof!"

# During class creation the metaclass prints:
# Creating class Dog
#   Bases: ()
#   Attributes: ['__module__', '__qualname__', 'bark']

fido = Dog()
print(fido.bark())  # Woof!

How a Metaclass Works

  • __new__(mcs, name, bases, namespace) creates the class object before __init__ runs.
  • mcs stands for “metaclass” (by convention, analogous to cls for classes and self for instances).
  • Parameters:
    • mcs – the metaclass itself
    • name – class name as a string
    • bases – tuple of base classes
    • namespace – dictionary of attributes and methods

You can also implement __init__ in a metaclass to perform additional initialization after the class has been created. This flexibility lets you inject behavior, enforce constraints, or automatically register classes.

Back to Blog

Related posts

Read more »

Java OOPS Concepts

!Forem Logohttps://media2.dev.to/dynamic/image/width=65,height=,fit=scale-down,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%...