~22 min · metaclass, type, class-creation, advanced
Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
Classes are objects too
In Python, a class is itself an object. When you write class Foo:, Python creates an object named Foo whose type is type. Just as instances are made by calling a class, classes are made by calling type. Foo = type("Foo", (), {}) is a manual class definition. type is the metaclass — "the class of classes."
What a metaclass lets you do
Customize what happens at class definition time. You can validate the class body, modify methods, register the class somewhere, add automatic attributes. Frameworks use metaclasses for things like Django's ORM (defining class User(Model): wires up SQL machinery), older Pydantic, abstract base class enforcement.
The cost — and why most code doesn't need it
Metaclasses are the most invasive customization Python offers. They run at class-definition time, change behavior in ways callers can't see in the class body, and create surprising interactions when combined. Tim Peters: "Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't."
Modern alternatives
Class decorators do most of what metaclasses do — they receive the constructed class and can modify it. __init_subclass__ (3.6+) is a hook that runs automatically when a subclass is created. __set_name__ (3.6+) is a hook on descriptors that runs when they're attached. For most use cases that historically called for a metaclass, one of these three is now the right tool.
War Story: Pippa's adapter ABC could have been a metaclass. It isn't, because abc.ABCMeta (the metaclass behind abc.ABC) handles the "require subclasses to implement abstract methods" behavior, and ABC hides the metaclass from us. The lesson: most metaclass needs are already solved by stdlib helpers.
Code
type as a metaclass — manually creating a class·python
# These two are equivalent
# Form 1
class Foo:
x = 10
def hello(self):
return "hi"
# Form 2
Foo2 = type("Foo2", (), {"x": 10, "hello": lambda self: "hi"})
f1 = Foo()
f2 = Foo2()
print(f1.x, f1.hello())
print(f2.x, f2.hello())
print(type(Foo)) # <class 'type'>
print(type(Foo2)) # <class 'type'>
A simple custom metaclass·python
class UpperAttrMeta(type):
def __new__(mcs, name, bases, namespace):
upper_namespace = {}
for key, value in namespace.items():
if not key.startswith("__"):
upper_namespace[key.upper()] = value
else:
upper_namespace[key] = value
return super().__new__(mcs, name, bases, upper_namespace)
class Foo(metaclass=UpperAttrMeta):
x = 10
def hello(self):
return "hi"
print(Foo.X) # 10 — renamed automatically
print(Foo().HELLO()) # 'hi' — method renamed too
__init_subclass__ — most metaclass uses replaced·python
class Plugin:
registry = {}
def __init_subclass__(cls, name=None, **kwargs):
super().__init_subclass__(**kwargs)
Plugin.registry[name or cls.__name__] = cls
class JsonPlugin(Plugin, name="json"):
def process(self, data):
import json
return json.dumps(data)
class YamlPlugin(Plugin, name="yaml"):
def process(self, data):
return f"yaml: {data}"
print(Plugin.registry)
# {'json': <class '...JsonPlugin'>, 'yaml': <class '...YamlPlugin'>}
# A subclass automatically registers itself — no metaclass required
Class decorator — also replaces many metaclass uses·python
def auto_repr(cls):
def __repr__(self):
attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{cls.__name__}({attrs})"
cls.__repr__ = __repr__
return cls
@auto_repr
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
print(Point(3, 4)) # Point(x=3, y=4)
# This used to be a metaclass exercise. Now it's a 4-line decorator.
# Most use cases: prefer this. Reach for metaclass only when you must.
Implement a Singleton metaclass that ensures any class using it can have only one instance — repeated calls to the constructor return the same instance. Then rewrite the singleton pattern as (a) a class decorator, and (b) using __new__. Compare the three approaches: which is clearest? Test by creating two instances and confirming they're the same object via is.
Progress
Progress is local-only — sign in to sync across devices.