Python Metaclasses Practice Problems & Exercises
Practice: Metaclasses
← Back to lessonEasy
Use type() to inspect the class and metaclass of a simple Dog class, then confirm that Dog is an instance of type.
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'>
TrueHints
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.
Create a Point class entirely using type() — no class statement. It must have an __init__ and a distance method.
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.0Hints
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.
Write a metaclass RegistryMeta that maintains a list of all class names that use it. Print the registry after defining three shape classes.
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
Build a metaclass SnakeCaseMeta that rejects any class definition containing camelCase method names. It should raise TypeError with a clear message.
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_caseHints
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.
Implement a SingletonMeta metaclass. Any class that uses it should only ever have one instance, regardless of how many times it is constructed.
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: sameHints
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.
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.
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.
Build FrozenClassMeta — a metaclass that prevents modification of class-level attributes after the class is defined.
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
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.
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.
Write OrderedABCMeta — a metaclass that ensures all @abstractmethod methods appear before any concrete methods in the class body. This enforces a strict definition order.
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.
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.
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']
TrueHints
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.
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.
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: 150Hints
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.
