Skip to main content

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; inner Ox.Meta is Meta for __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):

  1. Import builder module (class bodies of Builder/Descriptor run)
  2. Start Klass body (no class object yet)
  3. Descriptor()
  4. type.__new__ then __set_name__
  5. Builder.__init_subclass__(Klass)
  6. @deco
  7. main() / 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 =.

Quiz

When code runs

1 / 2

Configure __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.

Quiz

Metaclass politics

1 / 2

class Record(abc.ABC, metaclass=PersistentMeta)

Pick an answer to see if you are right.