Class Metaprogramming
Source: Fluent Python, 2nd Edition — Chapter 25 by Luciano Ramalho
3.6+ made first-edition class-builder recipes redundant. Prefer __set_name__, __init_subclass__, insertion-ordered dict, decorators — then a metaclass only if you must touch type.__new__ (usually __slots__). This is the last Fluent Python chapter in the course.
Classes are objects
dir omits most of these. Already seen: __class__, __name__, __mro__. Also:
__bases__— immediate bases__qualname__— PEP 3155; innerOx.MetaisMetafor__name____subclasses__()— live immediate subclasses (weak refs)mro()— interpreter calls this while building; a metaclass may override it
type(obj) is obj.__class__. type is also a class. Three arguments build a class:
MyClass = type('MyClass', (MySuperClass, MyMixin),
{'x': 42, 'x2': lambda self: self.x * 2})
Same as a class statement: name, bases (or (object,)), dict (callables → methods). Optional kwargs exist; skip them. type(int) is type; type(type) is type (magic, with object). User classes default to instances of type. Iterable.__class__ is ABCMeta; ABCMeta subclasses type — that is what a metaclass is.
Starter fakes __name__ but tag always returns 'x'. Use the three-arg type.
record_factory
Tiny @dataclass cousin: Dog = record_factory('Dog', 'name weight owner') — nice repr, unpackable, mutable, MRO just (Dog, object). type(cls_name, (object,), {__slots__, __init__, __iter__, __repr__}). Parse fields from a string or iterable; isidentifier(). __slots__ both saves RAM and freezes the attr set (else you’d write __setattr__). Not pickleable — see namedtuple guts if you care.
Cannot use __slots__ from __init_subclass__ or a decorator: too late; slots must be in the namespace type.__new__ sees.
__init_subclass__ — Checked
Like NamedTuple/@dataclass from a class body, but runtime constructors: title: str, year: int. Keyword-only __init__. Missing field → constructor no args (int() is 0). ... (Ellipsis) as kwargs.pop default vs real None. Field.__set__: callable constructor, reject type(None); value is ... → constructor(); else constructor(value) catching TypeError/ValueError (float(None) vs float('A')). Store instance.__dict__[name]. No __set_name__ — Checked.__init_subclass__ plants Fields.
__init_subclass__(subclass) is not a normal classmethod: first arg is the new child, never Checked itself. Still call super().__init_subclass__(). Do not @classmethod it (like __new__’s weirdness).
__setattr__ blocks unknown names and must call descriptor.__set__ by hand — otherwise __setattr__ swallows the overriding descriptor. _fields / _asdict leading _ like namedtuple. Mypy cannot check Checked.__init__(**kwargs). list[float] is list at runtime.
@checked decorator
Same Fields, inject _fields, __init__, __repr__, __setattr__, … then return cls. Prefer this when you must not take the only inheritance slot or fight a metaclass. @dataclass is 1200+ lines of the same idea.
__init_subclass__ vs decorator: after type.__new__. Order in Ramalho’s noisy demo (class that is both decorated and a Builder subclass — don’t ship that):
- Import builder module (class bodies of
Builder/Descriptorrun) - Start
Klassbody (no class object yet) Descriptor()type.__new__then__set_name__Builder.__init_subclass__(Klass)@decomain()/ instances:__init__, injected methods,__set__
Metaclasses 101
Peters: if you wonder, you don’t. Mill that builds mills. class Klass(Super, metaclass=Meta) → Meta.__new__(meta_cls, name, bases, cls_dict) then Super.__init_subclass__ then decorator then bind the name.
__prepare__(meta_cls, name, bases) (classmethod on the metaclass) returns the namespace mapping (was OrderedDict; 3.6+ dict is enough). NosyDict shows __module__/__qualname__ land before the body. After the body: __classcell__, then Meta.__new__. Pass a real dict to type.__new__ (UserDict.data).
MetaBunch: class attrs become defaults + __slots__; unexpected kwargs poke setattr to fail fast; __repr__ omits defaults. Copy dunders unless already injected.
CheckedMeta + slots: Field storage_name = '_' + name + __get__/setattr — slots cannot share the public name with the descriptor. Skip processing if '__slots__' already in cls_dict (the empty Checked base). Read __annotations__ from the dict, not get_type_hints (class doesn’t exist). Drop __setattr__ — slots refuse flavor =.
When code runs
1 / 2Configure __slots__ for a user subclass
Pick an answer to see if you are right.
Starter stores '8' or Ellipsis; missing must become 0.
Real world
Decorators, __set_name__, __init_subclass__, ordered dicts ate old metaclass jobs. MetaBunch from 2002 still runs. One metaclass per class — ABC + another meta → conflict; merge metas or drop one (deadline debt). Hide the meta behind a base class (Checked, Enum). Stdlib metas are implementation details.
AutoConst: __prepare__ → WilyDict with __missing__: first banana → 0, increment. Skip __dunder__ lookups (KeyError) or you eat __name__. Ideas: Flavor[2], iterate, Enum — __class_getitem__ (PEP 585) is the narrow class-level operator trick so list[int] needed no new builtin metaclass.
Uses that actually pay: register subclasses, structural checks, decorate many methods, ORM/persistence, class-level operators, import-time work. Martelli: shiny hammer. DHH: extract frameworks, don’t invent them in the app.
Metaclass politics
1 / 2class Record(abc.ABC, metaclass=PersistentMeta)
Pick an answer to see if you are right.