The deep end all 2 written
The protocols underneath properties and ORMs, metaclasses and why you probably should not, and the C boundary.
47 Descriptors: the protocol under property
Lesson 31 used @property to make
a method look like an attribute. That was not a special feature — it is one use of a protocol you can
implement yourself, and which turns out to be how self works too.
Attribute access is a method call
When you write obj.thing, Python looks for thing in the class. If what it finds
defines __get__, Python calls it instead of handing the object back. That object is a
descriptor.
class Constant:
"""The smallest possible descriptor."""
def __get__(self, obj, objtype=None):
return 42
class Circle:
answer = Constant()
c = Circle()
print("c.answer: ", c.answer)
print("what is actually stored in the class:", type(Circle.__dict__["answer"]).__name__)
print("the instance dict is empty:", c.__dict__)
print()
print("calling the protocol by hand:", Circle.__dict__["answer"].__get__(c, Circle))
test("attribute access ran __get__", c.answer, 42)
test("nothing was stored on the instance", c.__dict__, {})
test("what the class holds is the descriptor, not the value",
type(Circle.__dict__["answer"]).__name__, "Constant")
What it printed, when the build ran it
c.answer: 42
what is actually stored in the class: Constant
the instance dict is empty: {}
calling the protocol by hand: 42
ok attribute access ran __get__
ok nothing was stored on the instance
ok what the class holds is the descriptor, not the value
property is one of these
class Account:
def __init__(self, balance):
self._balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("balance cannot be negative")
self._balance = value
held = Account.__dict__["balance"]
print("what @property produced:", type(held).__name__)
print("defines __get__:", hasattr(held, "__get__"))
print("defines __set__:", hasattr(held, "__set__"))
print("defines __delete__:", hasattr(held, "__delete__"))
print()
a = Account(10)
print("a.balance:", a.balance)
print("by hand: ", held.__get__(a, Account))
test("property is an ordinary object in the class dict", type(held).__name__, "property")
test("it implements all three hooks",
(hasattr(held, "__get__"), hasattr(held, "__set__"), hasattr(held, "__delete__")),
(True, True, True))
test("and the protocol can be driven manually", held.__get__(a, Account), 10)
What it printed, when the build ran it
what @property produced: property defines __get__: True defines __set__: True defines __delete__: True a.balance: 10 by hand: 10 ok property is an ordinary object in the class dict ok it implements all three hooks ok and the protocol can be driven manually
Data and non-data descriptors, and who wins
A descriptor with __set__ (or __delete__) is a data descriptor
and takes priority over the instance's own dictionary. One with only __get__ is
non-data, and loses to it. This ordering is the mechanism behind several things that
otherwise look arbitrary.
class Data:
def __get__(self, obj, objtype=None):
return "from the data descriptor"
def __set__(self, obj, value):
obj.__dict__["stored"] = value
class NonData:
def __get__(self, obj, objtype=None):
return "from the non-data descriptor"
class Thing:
a = Data()
b = NonData()
t = Thing()
print("before anything is in the instance dict:")
print(" t.a ->", t.a)
print(" t.b ->", t.b)
t.__dict__["a"] = "from the instance dict"
t.__dict__["b"] = "from the instance dict"
print()
print("after writing both names straight into t.__dict__:")
print(" t.a ->", t.a)
print(" t.b ->", t.b)
test("a data descriptor cannot be shadowed by the instance", t.a, "from the data descriptor")
test("a non-data one can be", t.b, "from the instance dict")
test("and the instance dict really does hold both", sorted(t.__dict__), ["a", "b"])
What it printed, when the build ran it
before anything is in the instance dict: t.a -> from the data descriptor t.b -> from the non-data descriptor after writing both names straight into t.__dict__: t.a -> from the data descriptor t.b -> from the instance dict ok a data descriptor cannot be shadowed by the instance ok a non-data one can be ok and the instance dict really does hold both
Which explains functools.cached_property exactly. It is a non-data descriptor: the first
access computes the value and writes it into the instance dictionary, and from then on the dictionary wins
and the descriptor is never consulted again. No caching logic, just the lookup order.
from functools import cached_property
computed = []
class Report:
@cached_property
def total(self):
computed.append("ran")
return sum(range(1000))
r = Report()
print("instance dict before any access:", r.__dict__)
print("first access: ", r.total)
print("instance dict now:", r.__dict__)
print("second access:", r.total)
print("times the body ran:", len(computed))
print()
print("cached_property defines __set__?", hasattr(Report.__dict__["total"], "__set__"))
print("so after the first access, the instance dict shadows it.")
test("the body ran exactly once", len(computed), 1)
test("because the value moved into the instance dict", r.__dict__["total"], 499500)
test("which works only because it is a non-data descriptor",
hasattr(Report.__dict__["total"], "__set__"), False)
What it printed, when the build ran it
instance dict before any access: {}
first access: 499500
instance dict now: {'total': 499500}
second access: 499500
times the body ran: 1
cached_property defines __set__? False
so after the first access, the instance dict shadows it.
ok the body ran exactly once
ok because the value moved into the instance dict
ok which works only because it is a non-data descriptor
Where self comes from
A function is a non-data descriptor. Looking a method up on an instance calls the function's
__get__, which returns a bound method — the function with the instance already
attached. That is the entire mechanism.
class Greeter:
def greet(self, name):
return f"hello {name}, from {type(self).__name__}"
stored = Greeter.__dict__["greet"]
g = Greeter()
print("what the class dict holds:", type(stored).__name__)
print("does a function have __get__?", hasattr(stored, "__get__"))
print()
print("looked up on the instance: ", type(g.greet).__name__)
print("what it is bound to: ", type(g.greet.__self__).__name__)
print()
print("normal call: ", g.greet("ada"))
print("binding by hand: ", stored.__get__(g)("ada"))
print("no binding at all:", stored(g, "ada"))
test("a plain function is stored in the class", type(stored).__name__, "function")
test("looking it up on an instance produces a bound method", type(g.greet).__name__, "method")
test("the binding is __get__, and you can do it yourself", stored.__get__(g)("ada"), g.greet("ada"))
test("or skip it and pass self explicitly", stored(g, "x"), g.greet("x"))
What it printed, when the build ran it
what the class dict holds: function does a function have __get__? True looked up on the instance: method what it is bound to: Greeter normal call: hello ada, from Greeter binding by hand: hello ada, from Greeter no binding at all: hello ada, from Greeter ok a plain function is stored in the class ok looking it up on an instance produces a bound method ok the binding is __get__, and you can do it yourself ok or skip it and pass self explicitly
This is why self is a parameter and not a keyword. Nothing is injected.
The function takes an ordinary first argument, and attribute lookup happens to supply it. It is also why
staticmethod and classmethod exist: they are descriptors that bind
differently — staticmethod binds nothing, classmethod binds the class.
class C:
def instance_method(self): return "got an instance"
@staticmethod
def static_method(): return "got nothing"
@classmethod
def class_method(cls): return f"got {cls.__name__}"
for name in ("instance_method", "static_method", "class_method"):
stored = C.__dict__[name]
bound = stored.__get__(C(), C)
print(f"{name:<16} stored as {type(stored).__name__:<13} -> binds to {type(bound).__name__}")
print()
print("staticmethod, bound through the class:", C.__dict__["static_method"].__get__(None, C)())
print("classmethod, bound through the class: ", C.__dict__["class_method"].__get__(None, C)())
test("a staticmethod's __get__ gives back a plain function",
type(C.__dict__["static_method"].__get__(None, C)).__name__, "function")
test("a classmethod's gives a method bound to the class",
type(C.__dict__["class_method"].__get__(None, C)).__name__, "method")
test("and calling it receives the class", C.class_method(), "got C")
What it printed, when the build ran it
instance_method stored as function -> binds to method static_method stored as staticmethod -> binds to function class_method stored as classmethod -> binds to method staticmethod, bound through the class: got nothing classmethod, bound through the class: got C ok a staticmethod's __get__ gives back a plain function ok a classmethod's gives a method bound to the class ok and calling it receives the class
Writing one that is worth writing
The case for a descriptor of your own is repeated validation across several attributes, where a
property per attribute would mean copying the same three lines. First, though, the mistake
everybody makes.
class Broken:
"""Stores the value on the descriptor. Looks fine with one instance."""
def __init__(self):
self.value = None
def __get__(self, obj, objtype=None):
if obj is None:
return self
return self.value
def __set__(self, obj, value):
self.value = value # <- on the DESCRIPTOR, not the instance
class Item:
name = Broken()
one, two = Item(), Item()
one.name = "first"
two.name = "second"
print("one.name:", one.name)
print("two.name:", two.name)
print()
print("there is exactly one Broken object, living on the class:")
print(" ", Item.__dict__["name"])
print("so both instances have been writing to the same attribute.")
test("the second write overwrote the first", one.name, "second")
test("both instances read the same value", one.name == two.name, True)
test("the value ended up on the descriptor, not on either instance",
Item.__dict__["name"].value, "second")
test("and both instance dicts are empty", (one.__dict__, two.__dict__), ({}, {}))
What it printed, when the build ran it
one.name: second two.name: second there is exactly one Broken object, living on the class: <__main__.Broken object at 0x105784590> so both instances have been writing to the same attribute. ok the second write overwrote the first ok both instances read the same value ok the value ended up on the descriptor, not on either instance ok and both instance dicts are empty
The descriptor is created once, when the class body runs. Per-instance state has to live on the
instance. __set_name__ — called by Python at class creation, with the name the descriptor was
assigned to — is what makes that ergonomic.
class Positive:
"""A validated numeric field, reusable across attributes and classes."""
def __set_name__(self, owner, name):
# Called once, at class creation, with the attribute name.
self.public = name
self.private = "_" + name
def __get__(self, obj, objtype=None):
if obj is None:
return self # accessed on the class
return getattr(obj, self.private)
def __set__(self, obj, value):
if not isinstance(value, (int, float)):
raise TypeError(f"{self.public} must be a number, got {type(value).__name__}")
if value <= 0:
raise ValueError(f"{self.public} must be positive, got {value}")
setattr(obj, self.private, value) # on the INSTANCE
class LineItem:
price = Positive()
quantity = Positive()
def __init__(self, price, quantity):
self.price = price
self.quantity = quantity
@property
def total(self):
return self.price * self.quantity
book = LineItem(12.50, 3)
pen = LineItem(1.00, 10)
print("names bound by __set_name__:", LineItem.price.public, "and", LineItem.quantity.public)
print("book:", book.__dict__, "-> total", book.total)
print("pen: ", pen.__dict__, "-> total", pen.total)
print()
for bad in (-5, 0, "free"):
try:
book.price = bad
except (TypeError, ValueError) as exc:
print(f" price = {bad!r:<8} {type(exc).__name__}: {exc}")
test("the two instances keep separate values", (book.price, pen.price), (12.5, 1.0))
test("values are stored on the instance, under the private name",
sorted(book.__dict__), ["_price", "_quantity"])
test("validation names the attribute it rejected", LineItem.quantity.public, "quantity")
test("and the rejected write did not land", book.price, 12.5)
test("one descriptor class covers both fields",
type(LineItem.price) is type(LineItem.quantity), True)
What it printed, when the build ran it
names bound by __set_name__: price and quantity
book: {'_price': 12.5, '_quantity': 3} -> total 37.5
pen: {'_price': 1.0, '_quantity': 10} -> total 10.0
price = -5 ValueError: price must be positive, got -5
price = 0 ValueError: price must be positive, got 0
price = 'free' TypeError: price must be a number, got str
ok the two instances keep separate values
ok values are stored on the instance, under the private name
ok validation names the attribute it rejected
ok and the rejected write did not land
ok one descriptor class covers both fields
__delete__, for completeness
class Tracked:
def __set_name__(self, owner, name):
self.public, self.private = name, "_" + name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private)
def __set__(self, obj, value):
setattr(obj, self.private, value)
def __delete__(self, obj):
print(f" __delete__ ran for {self.public}")
delattr(obj, self.private)
class Row:
label = Tracked()
r = Row()
r.label = "x"
after_set = dict(r.__dict__)
print("after assignment:", after_set)
del r.label
print("after del: ", r.__dict__)
try:
r.label
missing = None
except AttributeError as exc:
missing = str(exc)
print("reading it now: ", missing)
test("__set__ stored it under the private name", after_set, {"_label": "x"})
test("del ran __delete__ and removed it", r.__dict__, {})
test("after which reading raises", "has no attribute" in missing, True)
What it printed, when the build ran it
after assignment: {'_label': 'x'}
__delete__ ran for label
after del: {}
reading it now: 'Row' object has no attribute '_label'
ok __set__ stored it under the private name
ok del ran __delete__ and removed it
ok after which reading raises
When to use which
| What you need | Reach for |
|---|---|
| A computed value that reads like an attribute | @property |
| The same, computed once and remembered | @functools.cached_property |
| The same validation on many attributes, or across classes | a descriptor |
| A plain record with type hints | @dataclass |
| Anything you can express with the four above | not a descriptor |
Descriptors are the machinery underneath ORMs and validation libraries, where one
IntegerField class serves thousands of attribute declarations. That is the shape that justifies
them. Two attributes on one class does not.
- A descriptor is any object in a class that defines
__get__. Attribute access calls it. __set__or__delete__makes it a data descriptor, which beats the instance dict.__get__alone loses to it — which is allcached_propertyis.- Functions are descriptors.
selfis__get__returning a bound method, and you can call it by hand. - The descriptor object is shared by every instance. Per-instance state goes on the instance.
__set_name__tells the descriptor what it was named, which is what makes a reusable field practical.- Use
propertyuntil you are writing it for the third time.
48 Metaclasses, and why you probably should not
A class is an object, and like every object it was made by something. That
something is type, and you can replace it. This lesson shows how, and then spends most of its
length on the two smaller hooks that made doing so unnecessary.
Classes are objects too
class Rectangle:
sides = 4
print("an instance's type:", type(Rectangle()).__name__)
print("the class's type: ", type(Rectangle).__name__)
print("type's own type: ", type(type).__name__)
print()
print("so a class is an instance of type:", isinstance(Rectangle, type))
print("and can be passed around like any value:")
for cls in (Rectangle, int, str):
print(" ", cls.__name__, "->", cls.__mro__[-1].__name__)
test("instances are instances of their class", isinstance(Rectangle(), Rectangle), True)
test("and the class is an instance of type", isinstance(Rectangle, type), True)
test("type is its own type", type(type) is type, True)
test("every class inherits from object", Rectangle.__mro__[-1], object)
What it printed, when the build ran it
an instance's type: Rectangle the class's type: type type's own type: type so a class is an instance of type: True and can be passed around like any value: Rectangle -> object int -> object str -> object ok instances are instances of their class ok and the class is an instance of type ok type is its own type ok every class inherits from object
The class statement is a function call
type with three arguments builds a class: a name, a tuple of bases, and a dictionary that
becomes the class body. The class keyword is more readable syntax for exactly this.
def area(self):
return self.width * self.height
# Built by calling type directly.
Made = type("Made", (object,), {"width": 2, "height": 3, "area": area})
# Built with the statement.
class Written:
width, height = 2, 3
def area(self):
return self.width * self.height
print("Made().area(): ", Made().area())
print("Written().area():", Written().area())
print()
print("both are classes: ", isinstance(Made, type), isinstance(Written, type))
print("both report type type: ", type(Made).__name__, type(Written).__name__)
print("Made's body came from a dict passed at runtime.")
test("the two are equivalent in behaviour", Made().area(), Written().area())
test("and indistinguishable in kind", type(Made) is type(Written), True)
test("the name is whatever you passed", Made.__name__, "Made")
test("a method built this way still binds self",
type(Made().area).__name__, "method")
What it printed, when the build ran it
Made().area(): 6 Written().area(): 6 both are classes: True True both report type type: type type Made's body came from a dict passed at runtime. ok the two are equivalent in behaviour ok and indistinguishable in kind ok the name is whatever you passed ok a method built this way still binds self
This is genuinely useful on its own. Building a class from a dict is how you generate
types from a schema, a database table or a config file — no metaclass required, just
type(...) called at runtime.
A metaclass, firing
Pass metaclass= and your class takes over class creation. Three hooks run, in this
order, once per class — not per instance.
class Recording(type):
@classmethod
def __prepare__(mcls, name, bases, **kwargs):
print(f" 1. __prepare__({name}) - hands back the mapping the body fills in")
return {}
def __new__(mcls, name, bases, namespace, **kwargs):
body = [k for k in namespace if not k.startswith("__")]
print(f" 2. __new__({name}) - body contains {body}")
cls = super().__new__(mcls, name, bases, namespace)
cls.made_by = mcls.__name__ # inject something
return cls
def __init__(cls, name, bases, namespace, **kwargs):
print(f" 3. __init__({name}) - the class now exists")
super().__init__(name, bases, namespace)
print("defining the class:")
class Widget(metaclass=Recording):
size = 1
def draw(self):
return "drawn"
print()
print("injected attribute:", Widget.made_by)
print("type of Widget: ", type(Widget).__name__)
print()
print("making instances:")
first, second = Widget(), Widget()
print(" nothing above re-ran.")
test("the metaclass injected an attribute", Widget.made_by, "Recording")
test("Widget's type is the metaclass, not type", type(Widget).__name__, "Recording")
test("but Widget is still a class", isinstance(Widget, type), True)
test("and instances are ordinary", first.draw(), "drawn")
test("creation hooks run per class, not per instance", type(first).made_by, "Recording")
What it printed, when the build ran it
defining the class: 1. __prepare__(Widget) - hands back the mapping the body fills in 2. __new__(Widget) - body contains ['size', 'draw'] 3. __init__(Widget) - the class now exists injected attribute: Recording type of Widget: Recording making instances: nothing above re-ran. ok the metaclass injected an attribute ok Widget's type is the metaclass, not type ok but Widget is still a class ok and instances are ordinary ok creation hooks run per class, not per instance
The thing people actually want: a registry
Nine times in ten, the reason someone reaches for a metaclass is "I want to know about every subclass". Here it is with a metaclass, which works.
class RegisteringMeta(type):
registry = {}
def __new__(mcls, name, bases, namespace, **kwargs):
cls = super().__new__(mcls, name, bases, namespace)
if bases: # skip the base class itself
# The key has to be guessed from the class name, because the class
# statement had no way to pass one in.
mcls.registry[name.removesuffix("Exporter").lower()] = cls
return cls
class Exporter(metaclass=RegisteringMeta):
def export(self, rows):
raise NotImplementedError
class CsvExporter(Exporter):
def export(self, rows):
return "\n".join(",".join(map(str, r)) for r in rows)
class TsvExporter(Exporter):
def export(self, rows):
return "\n".join("\t".join(map(str, r)) for r in rows)
print("registry:", {k: v.__name__ for k, v in RegisteringMeta.registry.items()})
chosen = RegisteringMeta.registry["csv"]()
print("csv export:", repr(chosen.export([[1, 2], [3, 4]])))
test("both subclasses registered themselves", sorted(RegisteringMeta.registry), ["csv", "tsv"])
test("the base class did not", "exporter" in RegisteringMeta.registry, False)
test("and the looked-up class works", chosen.export([[1, 2]]), "1,2")
What it printed, when the build ran it
registry: {'csv': 'CsvExporter', 'tsv': 'TsvExporter'}
csv export: '1,2\n3,4'
ok both subclasses registered themselves
ok the base class did not
ok and the looked-up class works
And here it is with __init_subclass__, which is an ordinary method on an ordinary class,
called by Python whenever the class is subclassed. Same result, no metaclass, and it accepts keyword
arguments from the class statement.
class Exporter:
registry = {}
def __init_subclass__(cls, /, format=None, **kwargs):
super().__init_subclass__(**kwargs)
if format is None:
raise TypeError(f"{cls.__name__} must declare format=")
Exporter.registry[format] = cls
def export(self, rows):
raise NotImplementedError
class Csv(Exporter, format="csv"):
def export(self, rows):
return "\n".join(",".join(map(str, r)) for r in rows)
class Tsv(Exporter, format="tsv"):
def export(self, rows):
return "\n".join("\t".join(map(str, r)) for r in rows)
print("registry:", {k: v.__name__ for k, v in Exporter.registry.items()})
# Forgetting the keyword is caught at class definition time, not at first use.
try:
class Broken(Exporter):
pass
complaint = None
except TypeError as exc:
complaint = str(exc)
print("forgetting format=:", complaint)
print()
print("__init_subclass__ is implicitly a classmethod:",
type(Exporter.__dict__["__init_subclass__"]).__name__)
test("the registry is keyed by the declared format", sorted(Exporter.registry), ["csv", "tsv"])
test("the base class is not in it", Exporter in Exporter.registry.values(), False)
test("a missing keyword fails at definition time", complaint, "Broken must declare format=")
test("and no metaclass was involved", type(Csv) is type, True)
What it printed, when the build ran it
registry: {'csv': 'Csv', 'tsv': 'Tsv'}
forgetting format=: Broken must declare format=
__init_subclass__ is implicitly a classmethod: classmethod
ok the registry is keyed by the declared format
ok the base class is not in it
ok a missing keyword fails at definition time
ok and no metaclass was involved
Two hooks removed most of the need for metaclasses.
__init_subclass__ (3.6) runs when your class is subclassed;
__set_name__ (3.6, and the previous lesson) tells a class
attribute what it was named. Between them they cover registries, subclass validation, and field
declaration — which was nearly the whole list.
A metaclass you already use
abc.ABC is a class whose metaclass is ABCMeta, and it hooks instantiation to
refuse classes with unimplemented abstract methods. This is a good use: it is not decoration, it changes
what the class is.
from abc import ABC, abstractmethod
class Store(ABC):
@abstractmethod
def get(self, key): ...
@abstractmethod
def put(self, key, value): ...
def get_or(self, key, default): # concrete, built on the abstract pair
try:
return self.get(key)
except KeyError:
return default
class Memory(Store):
def __init__(self):
self.data = {}
def get(self, key):
return self.data[key]
def put(self, key, value):
self.data[key] = value
m = Memory()
m.put("a", 1)
print("the complete subclass works:", m.get("a"), "|", m.get_or("b", "missing"))
print("metaclass of Store:", type(Store).__name__)
print("abstract methods: ", sorted(Store.__abstractmethods__))
print()
for cls, label in ((Store, "the abstract base"), (type("Half", (Store,), {"get": lambda s, k: None}), "a half-finished subclass")):
try:
cls()
print(f"{label}: instantiated")
except TypeError as exc:
print(f"{label}: TypeError: {exc}")
test("ABC brings a metaclass with it", type(Store).__name__, "ABCMeta")
test("it records what is unimplemented", sorted(Store.__abstractmethods__), ["get", "put"])
test("a complete subclass has nothing left abstract", Memory.__abstractmethods__, frozenset())
test("and inherits the concrete helper", m.get_or("nope", 0), 0)
What it printed, when the build ran it
the complete subclass works: 1 | missing metaclass of Store: ABCMeta abstract methods: ['get', 'put'] the abstract base: TypeError: Can't instantiate abstract class Store without an implementation for abstract methods 'get', 'put' a half-finished subclass: TypeError: Can't instantiate abstract class Half without an implementation for abstract method 'put' ok ABC brings a metaclass with it ok it records what is unimplemented ok a complete subclass has nothing left abstract ok and inherits the concrete helper
Why they do not compose
A class has exactly one metaclass. Inherit from two classes with unrelated metaclasses and there is no answer — so two libraries that each use one cannot be mixed.
class Serialisable(type): pass
class Observable(type): pass
class Model(metaclass=Serialisable): pass
class Widget(metaclass=Observable): pass
# A class that wants to be both.
class Form(Model, Widget): pass
The traceback it really produced, when the build ran it
Traceback (most recent call last):
File "example.py", line 8, in <module>
class Form(Model, Widget): pass
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
That message is the strongest practical argument against writing one. A decorator composes; a
__init_subclass__ cooperates through super(); a metaclass claims a slot that
nothing else can share.
The singleton, and why it is the wrong example
Every metaclass tutorial reaches for a singleton. It does work — the metaclass'
__call__ runs when you call the class.
class Single(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Settings(metaclass=Single):
def __init__(self):
self.loaded = True
first, second = Settings(), Settings()
print("same object:", first is second)
print("Settings() calls type(Settings).__call__:", type(Settings).__call__ is Single.__call__)
# What the language already gives you, for the same purpose:
# a module. Imported once, cached in sys.modules, shared by every importer.
import sys
print()
print("sys is the same object everywhere it is imported:", sys is sys.modules["sys"])
test("the metaclass made it a singleton", first is second, True)
test("calling the class goes through the metaclass", type(Settings).__call__ is Single.__call__, True)
test("while a module is already a singleton, for free", sys is sys.modules["sys"], True)
What it printed, when the build ran it
same object: True Settings() calls type(Settings).__call__: True sys is the same object everywhere it is imported: True ok the metaclass made it a singleton ok calling the class goes through the metaclass ok while a module is already a singleton, for free
A module already gives you one instance, shared, lazily initialised, with no machinery — and unlike the
metaclass version it does not surprise the next reader by making Settings() not build a new
object. Which is the general shape of the argument: almost every metaclass makes a familiar syntax mean
something unfamiliar.
When to use one
| You want to… | Use |
|---|---|
| know about subclasses, or validate them | __init_subclass__ |
| give a class attribute its own name | __set_name__ |
| add or wrap methods after the fact | a class decorator |
| generate classes from data | type(name, bases, ns) |
| require methods to exist before instantiation | abc.ABC |
change what class itself means, for a framework | a metaclass |
Only the last row justifies one, and the honest version of that row is "you are writing something like Django's ORM or an enum implementation". Everything above it is simpler, composes with other code, and does not surprise anyone reading your class.
- Classes are objects; their type is
type;type(name, bases, ns)creates one. - A metaclass intercepts class creation via
__prepare__,__new__and__init__— once per class, not per instance. __init_subclass__and__set_name__replaced most legitimate uses, and they cooperate throughsuper().ABCMetais the metaclass worth knowing: it refuses to instantiate an incomplete class.- One class, one metaclass. Two unrelated ones cannot be combined — which is why frameworks that use them do not mix.
- If a decorator, a base class or a function would do, use that.