Yesterday’s models repeated themselves:
class Post(Model):
table = "post"
columns = ("title", "views")
columns restates what the class body already says, and forgetting a name there
is a bug the library can’t see. Worse, yesterday’s cache fix relied on everyone
remembering to key by class. Both are jobs for the moment the class is created.
What class actually does
A class statement is three steps: run the body as code, collect the resulting
namespace, and hand it to the metaclass (type, normally) to build the class
object. Two hooks let you join in without writing a metaclass at all.
__set_name__runs on each object in the namespace that defines it, right after the class exists. That’s how a field learns its name.__init_subclass__runs on the parent class, once per subclass, with the new class as its argument.
class Watcher:
def __set_name__(self, owner, name):
print(f"3. __set_name__ for {name}")
class Base:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
print(f"4. __init_subclass__ sees {cls.__name__}, fields: "
f"{[k for k, v in vars(cls).items() if isinstance(v, Watcher)]}")
print("1. before the class statement")
class Child(Base):
print("2. inside the class body")
field = Watcher()
print("5. after the class statement")The body runs first, then __set_name__ for each descriptor, then
__init_subclass__, and by that point the class is complete: the namespace is
populated and the MRO is set. That makes it the right place to check a model
over and to add whatever the library needs.
It's implicitly a classmethod
__init_subclass__ is turned into a classmethod for you, so it takes the new
subclass as cls. Writing @classmethod yourself is harmless but unnecessary.
It is not called for the class that defines it — only for its subclasses.
Class keyword arguments
A class statement can take keyword arguments, and they arrive in
__init_subclass__. That’s how table="post" in the class header works:
class Model:
def __init_subclass__(cls, table=None, **kwargs):
super().__init_subclass__(**kwargs)
cls.table = table or cls.__name__.lower()
class Post(Model, table="post"):
...
Passing **kwargs on to super() is not optional politeness. If a later class
in the MRO also expects a keyword, swallowing it silently breaks it — and
object.__init_subclass__ raises TypeError for anything left over, which is
how a typo in a class keyword gets reported instead of ignored.
class Model:
def __init_subclass__(cls, table=None, **kwargs):
super().__init_subclass__(**kwargs)
cls.table = table or cls.__name__.lower()
class Post(Model, table="post"):
pass
class Comment(Model): # no keyword: the default applies
pass
print(Post.table, Comment.table)
try:
class Broken(Model, tabel="post"): # a typo
pass
except TypeError as error:
print("TypeError:", error)The typo is caught at import time. The message —
Broken.__init_subclass__() takes no keyword arguments — comes from
object.__init_subclass__, the last link in the chain, and doesn’t name tabel
or point at the header; it is nonetheless an error rather than silence. A
library that swallowed **kwargs instead of passing them up would write to a
table called broken and leave someone debugging their data instead of their
spelling.
Collecting fields, including inherited ones
Walking vars(cls) finds the fields written in this class body, but a model
that inherits from another model should keep its parent’s fields too. The MRO,
reversed, gives base classes first, so a subclass can override a field by
redefining the name:
cls.fields = {
name: value
for klass in reversed(cls.__mro__)
for name, value in vars(klass).items()
if isinstance(value, Field)
}
class Field:
def __init__(self, label):
self.label = label
def __set_name__(self, owner, name):
self.name = name
def __repr__(self):
return f"Field({self.label!r})"
class Model:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.fields = {
name: value
for klass in reversed(cls.__mro__)
for name, value in vars(klass).items()
if isinstance(value, Field)
}
class Content(Model):
title = Field("base title")
created = Field("created")
class Post(Content):
title = Field("overridden title") # same name, new field
views = Field("views")
print("Content:", Content.fields)
print("Post: ", Post.fields)
print("order: ", tuple(Post.fields))Post.fields has three entries: the overridden title, the inherited
created and the new views. Order follows insertion, and the MRO is walked
from object downwards, so the order is the one the base class established —
title is still first, because Content introduced it — while the value for
an overridden name comes from the most derived class.
The two jobs that fix yesterday’s bugs
cls._instances = {} # its own cache, not the base class's
Model.registry[cls.table] = cls # one registry, on purpose
The first line is the real fix for yesterday’s shared cache: every model gets a
fresh dictionary as it is defined, so Post._instances is User._instances is
False and keys can go back to being plain ids. The library guarantees it
instead of asking each model’s author to remember.
The second line is a deliberately shared dictionary, on Model itself, so
Model.registry["post"] finds the class. Same mechanism, opposite decision —
which is the point. Shared mutable class state is a bug when it’s accidental and
a feature when it’s the thing you meant.
A registry keeps classes alive
Model.registry holds a strong reference to every model ever defined. For
classes that’s usually fine — they live as long as their module — but it does
mean a test that defines a model inside a function leaks it into the registry,
and a second definition with the same table name silently replaces the first.
Worth a documented note, and a check if you want one.
Going further: why not a metaclass?
Everything above can be done in a metaclass, and for twenty years it was. It is
now the wrong default, for one practical reason: metaclasses conflict. A
class can have only one, and its metaclass must be a subclass of the
metaclasses of all its bases. Combine your ORM model with any other library that
uses one and the user gets TypeError: metaclass conflict, which they cannot
fix from their side.
__init_subclass__ is ordinary inheritance and composes without drama.
What still needs a metaclass is rare and specific: changing the class namespace
before the body runs (__prepare__), controlling isinstance with
__instancecheck__, or altering how the class object itself is constructed.
Field collection, validation, registration and defaults are not on that list.
A model base that catches mistakes early
Complete __init_subclass__. Beyond collecting fields, it enforces two rules of
this library — each caught when the model is defined, not when it’s used.
import unittest
class Field:
def __set_name__(self, owner, name):
self.name = name
class Model:
registry = {}
def __init_subclass__(cls, table=None, **kwargs):
"""TODO:
- pass **kwargs to super().__init_subclass__
- set cls.table from the keyword, defaulting to the lowercased class name
- collect cls.fields from the whole MRO, base classes first
- give cls its own empty _instances dict
- refuse a model with no fields (ValueError)
- refuse a table name already in Model.registry (ValueError), then register
"""
raise NotImplementedError("__init_subclass__")
class Models(unittest.TestCase):
def setUp(self):
Model.registry.clear()
def define_post(self, table="post"):
class Post(Model, table=table):
title = Field()
views = Field()
return Post
def test_fields_and_table(self):
Post = self.define_post()
self.assertEqual(tuple(Post.fields), ("title", "views"))
self.assertEqual(Post.table, "post")
def test_the_table_defaults_to_the_class_name(self):
class Comment(Model):
body = Field()
self.assertEqual(Comment.table, "comment")
def test_each_model_has_its_own_cache(self):
first = self.define_post("post")
second = self.define_post("other")
self.assertEqual(first._instances, {})
self.assertIsNot(first._instances, second._instances)
self.assertIsNot(first._instances, Model.__dict__.get("_instances"))
def test_fields_are_inherited(self):
Post = self.define_post()
class Draft(Post, table="draft"):
note = Field()
self.assertEqual(tuple(Draft.fields), ("title", "views", "note"))
def test_a_model_without_fields_is_refused(self):
with self.assertRaises(ValueError):
class Empty(Model, table="empty"):
pass
def test_two_models_may_not_claim_one_table(self):
self.define_post("post")
with self.assertRaises(ValueError):
self.define_post("post")
def test_registered_by_table_name(self):
Post = self.define_post()
self.assertIs(Model.registry["post"], Post)
unittest.TextTestRunner(verbosity=2).run(
unittest.defaultTestLoader.loadTestsFromTestCase(Models)
)Hint 1: the idea
__init_subclass__ runs once for each subclass, the moment its class
statement finishes — which is why it’s where a library checks a model’s
definition. Everything it sets goes on cls, the new subclass. “Fields from the
whole MRO, base classes first” means walking cls.__mro__ from the far end,
looking for Field objects in each class’s own namespace.
Hint 2: a first step
Do the short lines first — super().__init_subclass__(**kwargs), cls.table,
cls._instances = {}, and registering — and get the table and cache tests
passing. Then the fields: vars(klass) is one class’s own namespace, and
reversed(cls.__mro__) visits the base classes first.
Show one solution
def __init_subclass__(cls, table=None, **kwargs):
super().__init_subclass__(**kwargs)
cls.table = table or cls.__name__.lower()
cls.fields = {
name: value
for klass in reversed(cls.__mro__)
for name, value in vars(klass).items()
if isinstance(value, Field)
}
cls._instances = {}
if not cls.fields:
raise ValueError(f"{cls.__name__} has no fields")
if cls.table in Model.registry:
taken = Model.registry[cls.table].__name__
raise ValueError(f"table {cls.table!r} is already used by {taken}")
Model.registry[cls.table] = clsThe order matters in two places. cls.fields has to be built before the
“no fields” check, obviously — but also, both checks come after the
assignments, so a model that fails one is still fully formed when the exception
is raised. That keeps the traceback readable in a test like the one above, where
the class object is examined after the failure.
The duplicate-table check reads the registry before writing to it, which is what makes it a check rather than a race with itself.
Raising here means a broken model can’t be imported. That’s the harshest feedback a library can give, and the right one for a mistake that would otherwise corrupt data.
What you learned
__set_name__then__init_subclass__run as a class is created, and both see a finished namespace.- Class keyword arguments arrive in
__init_subclass__; passing**kwargsup is what turns a typo into aTypeError. - Walking the reversed MRO collects inherited fields and lets a subclass override one.
- Per-subclass state is created here, which is how the library guarantees an invariant instead of documenting it.
- Metaclasses conflict;
__init_subclass__is ordinary inheritance and should be your default.