Skip to main content

Python Metaclasses Practice Problems & Exercises

Practice: Metaclasses

11 problems3 Easy4 Medium4 Hard70–90 min
← Back to lesson

Easy

#1Inspect a Class with type()Easy
metaclasstypeintrospection

Use type() to inspect the class and metaclass of a simple Dog class, then confirm that Dog is an instance of type.

Python
class Dog:
    species = "Canis lupus familiaris"

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

rex = Dog("Rex")

# What is the type of the instance?
print(type(rex))

# What is the type of the class itself?
print(type(Dog))

# Is Dog an instance of type?
print(isinstance(Dog, type))
Expected Output
<class '__main__.Dog'>
<class 'type'>
True
Hints

Hint 1: type() called on an instance returns the class. type() called on the class returns the metaclass.

Hint 2: All ordinary classes in Python 3 are instances of type.


#2Create a Class Dynamically with type()Easy
metaclasstypedynamic-class

Create a Point class entirely using type() — no class statement. It must have an __init__ and a distance method.

Python
import math

def point_init(self, x, y):
    self.x = x
    self.y = y

def point_repr(self):
    return f"Point(x={self.x}, y={self.y})"

def point_distance(self):
    return math.sqrt(self.x ** 2 + self.y ** 2)

# Build the class with type()
Point = type(
    "Point",
    (object,),
    {
        "__init__": point_init,
        "__repr__": point_repr,
        "distance": point_distance,
    },
)

p = Point(3, 4)
print(p)
print(f"distance = {p.distance()}")
Expected Output
Point(x=3, y=4)
distance = 5.0
Hints

Hint 1: type(name, bases, namespace) is the three-argument form that creates a new class.

Hint 2: Pass methods as regular functions in the namespace dict. They become unbound methods on the new class.


#3Class Attribute Registry via __init__Easy
metaclass__init__registry

Write a metaclass RegistryMeta that maintains a list of all class names that use it. Print the registry after defining three shape classes.

Python
class RegistryMeta(type):
    registry = []

    def __init__(cls, name, bases, namespace):
        super().__init__(name, bases, namespace)
        # Don't register the base class itself
        if bases:
            RegistryMeta.registry.append(name)


class Shape(metaclass=RegistryMeta):
    pass

class Circle(Shape):
    pass

class Rectangle(Shape):
    pass

class Triangle(Shape):
    pass

print(f"Registered classes: {RegistryMeta.registry}")
Expected Output
Registered classes: ['Circle', 'Rectangle', 'Triangle']
Hints

Hint 1: A metaclass __init__ is called after the class object is created. self here is the new class.

Hint 2: Store each class name in a list on the metaclass itself so all subclasses share one registry.


Medium

#4Enforce Method Naming ConventionMedium
metaclass__new__validation

Build a metaclass SnakeCaseMeta that rejects any class definition containing camelCase method names. It should raise TypeError with a clear message.

Python
class SnakeCaseMeta(type):
    def __new__(mcs, name, bases, namespace):
        for key, value in namespace.items():
            if key.startswith("_"):
                continue
            if callable(value) and key != key.lower():
                raise TypeError(
                    f"Method '{key}' violates naming convention — use snake_case"
                )
        return super().__new__(mcs, name, bases, namespace)


class APIClient(metaclass=SnakeCaseMeta):
    def fetch_data(self):
        return "data"

    def parse_response(self):
        return "parsed"

print("APIClient created successfully")

try:
    class BadClient(metaclass=SnakeCaseMeta):
        def GetData(self):
            return "data"
except TypeError as e:
    print(f"TypeError: {e}")
Expected Output
APIClient created successfully
TypeError: Method 'GetData' violates naming convention — use snake_case
Hints

Hint 1: Override __new__ in the metaclass, iterate over the namespace dict, and check each key that is callable.

Hint 2: A method name is snake_case if name == name.lower(). Raise TypeError before calling super().__new__ to block the class.


#5Singleton via MetaclassMedium
metaclasssingletondesign-pattern

Implement a SingletonMeta metaclass. Any class that uses it should only ever have one instance, regardless of how many times it is constructed.

Python
class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            # Store on the metaclass-level dict
            SingletonMeta._instances[cls] = super().__call__(*args, **kwargs)
        return SingletonMeta._instances[cls]


class Config(metaclass=SingletonMeta):
    def __init__(self, env="production"):
        self.env = env


a = Config("development")
b = Config("staging")

print(a is b)           # True — same object
print(id(a) == id(b))  # True

a.label = "same"
print(f"Config ID: {b.label}")
Expected Output
True
True
Config ID: same
Hints

Hint 1: Override __call__ in the metaclass. __call__ is invoked when you do MyClass(). Check an _instances dict keyed by cls.

Hint 2: If the key is already in _instances, return it. Otherwise call super().__call__() to create the instance and cache it.


#6Auto-Generate __repr__ for Dataclass-Like ClassesMedium
metaclass__repr__automation

Write a metaclass AutoReprMeta that automatically generates a __repr__ from the class __annotations__. Any class using it gets a clean repr without writing one manually.

Python
class AutoReprMeta(type):
    def __new__(mcs, name, bases, namespace):
        annotations = namespace.get("__annotations__", {})
        fields = list(annotations.keys())

        def auto_repr(self):
            parts = []
            for field in fields:
                val = getattr(self, field, None)
                parts.append(f"{field}={val!r}")
            return f"{name}(" + ", ".join(parts) + ")"

        namespace["__repr__"] = auto_repr
        return super().__new__(mcs, name, bases, namespace)


class Employee(metaclass=AutoReprMeta):
    name: str
    role: str
    salary: int

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


emp = Employee("Alice", "Engineer", 95000)
print(repr(emp))
Expected Output
Employee(name='Alice', role='Engineer', salary=95000)
Hints

Hint 1: In the metaclass __new__, inspect the namespace for any __annotations__. Generate a __repr__ function that iterates over those annotation keys.

Hint 2: Use a closure or a plain function that reads self.__dict__ filtered by the annotation keys.


#7Immutable Class Attributes via MetaclassMedium
metaclass__setattr__immutability

Build FrozenClassMeta — a metaclass that prevents modification of class-level attributes after the class is defined.

Python
class FrozenClassMeta(type):
    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        # Capture the initial public class attribute names
        frozen = frozenset(k for k in namespace if not k.startswith("__"))
        cls._frozen_attrs = frozen
        return cls

    def __setattr__(cls, name, value):
        if name != "_frozen_attrs" and hasattr(cls, "_frozen_attrs") and name in cls._frozen_attrs:
            raise AttributeError(f"Cannot modify frozen attribute '{name}'")
        super().__setattr__(name, value)


class Constants(metaclass=FrozenClassMeta):
    pi = 3.14159
    e = 2.71828


print(f"pi = {Constants.pi}")

try:
    Constants.pi = 3.0
except AttributeError as e:
    print(f"AttributeError: {e}")
Expected Output
pi = 3.14159
AttributeError: Cannot modify frozen attribute 'pi'
Hints

Hint 1: Override __setattr__ on the metaclass (not the class). This intercepts attribute assignment on the class object itself.

Hint 2: Track which attributes existed at class creation time in a frozenset, and block changes to those names after creation.


Hard

#8ORM-Style Column Descriptor MetaclassHard
metaclassORMdescriptortype-validation

Build a minimal ORM-style system. A ModelMeta metaclass should scan class annotations and convert bare type annotations into typed Column descriptors that validate on assignment. Provide __repr__ automatically.

Python
class Column:
    def __init__(self, expected_type, name=None):
        self.expected_type = expected_type
        self.name = name

    def __set_name__(self, owner, name):
        self.name = name
        self.private = "_col_" + name

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.private, None)

    def __set__(self, obj, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(
                f"Field '{self.name}' expects {self.expected_type}, got {type(value)}"
            )
        setattr(obj, self.private, value)


class ModelMeta(type):
    def __new__(mcs, name, bases, namespace):
        annotations = namespace.get("__annotations__", {})
        fields = []
        for field_name, field_type in annotations.items():
            if not isinstance(namespace.get(field_name), Column):
                col = Column(field_type)
                col.name = field_name
                col.private = "_col_" + field_name
                namespace[field_name] = col
            fields.append(field_name)

        def auto_repr(self):
            parts = [f"{f}={getattr(self, f)!r}" for f in fields]
            return f"{name}(" + ", ".join(parts) + ")"

        namespace["__repr__"] = auto_repr
        namespace["_fields"] = fields
        return super().__new__(mcs, name, bases, namespace)


class Model(metaclass=ModelMeta):
    pass


class User(Model):
    id: int
    name: str
    age: int

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


u = User(1, "Alice", 30)
print(u)

try:
    u.age = "thirty"
except TypeError as e:
    print(f"TypeError: {e}")
Expected Output
User(id=1, name='Alice', age=30)
TypeError: Field 'age' expects <class 'int'>, got <class 'str'>
Hints

Hint 1: Create a Column descriptor that stores the expected type. In __set__ validate isinstance(value, self.expected_type) and raise TypeError if not.

Hint 2: In the metaclass __new__, scan annotations and auto-wrap each bare type annotation into a Column descriptor stored in the class namespace.

Hint 3: Generate __repr__ from the annotation keys so the output is clean.


#9Method Order Enforcer — Abstract-Before-ConcreteHard
metaclass__new__abstractordering

Write OrderedABCMeta — a metaclass that ensures all @abstractmethod methods appear before any concrete methods in the class body. This enforces a strict definition order.

Python
from abc import abstractmethod


class OrderedABCMeta(type):
    def __new__(mcs, name, bases, namespace):
        seen_concrete = None
        for key, value in namespace.items():
            if key.startswith("_"):
                continue
            is_abstract = getattr(value, "__isabstractmethod__", False)
            if is_abstract and seen_concrete is not None:
                raise TypeError(
                    f"In '{name}': concrete method '{seen_concrete}' defined before "
                    f"abstract method '{key}'"
                )
            if not is_abstract and callable(value):
                seen_concrete = key
        return super().__new__(mcs, name, bases, namespace)


class Pipeline(metaclass=OrderedABCMeta):
    @abstractmethod
    def validate(self):
        pass

    @abstractmethod
    def transform(self):
        pass

    def run(self):
        self.validate()
        self.transform()


print("Pipeline created OK")

try:
    class BadPipeline(metaclass=OrderedABCMeta):
        def run(self):
            pass

        @abstractmethod
        def validate(self):
            pass
except TypeError as e:
    print(f"TypeError: {e}")
Expected Output
Pipeline created OK
TypeError: In 'BadPipeline': concrete method 'run' defined before abstract method 'validate'
Hints

Hint 1: Iterate over the namespace items in definition order (dicts preserve insertion order in Python 3.7+). Track whether you have seen a concrete method before an abstractmethod.

Hint 2: Detect abstractmethods by checking getattr(value, "__isabstractmethod__", False).

Hint 3: Raise TypeError before calling super().__new__ so the class never gets created.


#10Cooperative Metaclass — Merging Two MetaclassesHard
metaclasscooperativemultiple-inheritanceMRO

You have two metaclasses — LoggingMeta and RegistryMeta — each with its own behavior. Create a CombinedMeta that inherits from both and apply it to a Service class so both behaviors fire.

Python
class LoggingMeta(type):
    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        print(f"LoggingMeta: class '{name}' created")
        return cls


class RegistryMeta(type):
    registry = []

    def __init__(cls, name, bases, namespace):
        super().__init__(name, bases, namespace)
        if bases:
            RegistryMeta.registry.append(name)
            print(f"RegistryMeta: '{name}' registered")


# Cooperative combined metaclass
class CombinedMeta(LoggingMeta, RegistryMeta):
    pass


class BaseService(metaclass=CombinedMeta):
    pass


class Service(BaseService):
    def handle(self):
        return "handled"


print(RegistryMeta.registry)
print(isinstance(Service, CombinedMeta))
Expected Output
LoggingMeta: class 'Service' created
RegistryMeta: 'Service' registered
['Service']
True
Hints

Hint 1: When two metaclasses conflict Python raises TypeError. Resolve it by creating a combined metaclass that inherits from both: class CombinedMeta(LoggingMeta, RegistryMeta): pass

Hint 2: Each metaclass must call super().__new__ and super().__init__ for cooperative multiple inheritance to work correctly.

Hint 3: The combined metaclass resolves the MRO so both __new__ and __init__ chains are called.


#11Attribute Access Auditing MetaclassHard
metaclass__getattribute__auditingproxy

Build AuditMeta — a metaclass that wraps every public method of a class with an audit logger that prints the method name whenever it is called. Also log attribute reads on instances.

Python
import functools


class AuditMeta(type):
    def __new__(mcs, name, bases, namespace):
        audited = {}
        for key, value in namespace.items():
            if not key.startswith("_") and callable(value):
                # Wrap the method with an audit log
                @functools.wraps(value)
                def make_audited(fn, cls_name, method_name):
                    @functools.wraps(fn)
                    def wrapper(*args, **kwargs):
                        print(f"AUDIT: '{cls_name}.{method_name}' called")
                        return fn(*args, **kwargs)
                    return wrapper
                audited[key] = make_audited(value, name, key)
            else:
                audited[key] = value

        # Wrap __getattribute__ to log non-callable attribute reads
        original_getattribute = namespace.get("__getattribute__", object.__getattribute__)

        def audited_getattribute(self, attr_name):
            value = original_getattribute(self, attr_name)
            if not attr_name.startswith("_") and not callable(value):
                print(f"AUDIT: '{name}.{attr_name}' accessed")
            return value

        audited["__getattribute__"] = audited_getattribute
        return super().__new__(mcs, name, bases, audited)


class BankAccount(metaclass=AuditMeta):
    def __init__(self, balance):
        # Bypass audit during init by using object.__setattr__
        object.__setattr__(self, "balance", balance)

    def deposit(self, amount):
        object.__setattr__(self, "balance", object.__getattribute__(self, "balance") + amount)

    def withdraw(self, amount):
        object.__setattr__(self, "balance", object.__getattribute__(self, "balance") - amount)


acct = BankAccount(100)
acct.deposit(100)
acct.withdraw(50)
print(f"Balance: {acct.balance}")
Expected Output
AUDIT: 'BankAccount.deposit' called
AUDIT: 'BankAccount.withdraw' called
AUDIT: 'BankAccount.balance' accessed
Balance: 150
Hints

Hint 1: Override __getattribute__ on the metaclass to intercept attribute access on the class. But be careful — this intercepts class-level access, not instance-level.

Hint 2: A cleaner approach: in the metaclass __new__, wrap every callable in the namespace with a logging wrapper using functools.wraps.

Hint 3: Use a property or __getattr__ on instances for attribute access logging. Wrap methods at class creation time.

© 2026 EngineersOfAI. All rights reserved.