Customising classes
Hook into class creation with class decorators, __init_subclass__ and __set_name__, make a class subscriptable with __class_getitem__, and fix an instance's attributes with __slots__.
What you will learn
- Write a class decorator that adds behaviour to a class
- Build a plugin registry with __init_subclass__ and class keyword arguments
- Reject a badly defined subclass at definition time
- Describe the order in which class creation hooks run
- Use __class_getitem__ and __slots__, and explain how __slots__ uses descriptors
On this page
A class statement is code that runs. Python executes the body, builds a class object, and then gives several hooks a chance to look at it or change it. This lesson covers the hooks you should reach for first. They handle almost everything people once wrote metaclasses for, and the next lesson shows the rest. Hooks on individual attribute reads, __getattr__ and __getattribute__, were in Descriptors.
Class decorators
A class decorator works like the function decorators from decorators: @deco above class C means C = deco(C). The decorator receives the finished class and returns it, usually after adding something:
def auto_repr(cls): def __repr__(self): fields = ", ".join(f"{k}={v!r}" for k, v in vars(self).items()) return f"{type(self).__name__}({fields})"
cls.__repr__ = __repr__ return cls
@auto_reprclass Point: def __init__(self, x, y): self.x = x self.y = y
print(Point(1, 2))print([Point(0, 0), Point(3, 4)])Point(x=1, y=2)[Point(x=0, y=0), Point(x=3, y=4)]@dataclass is the most famous class decorator: it reads the annotations and writes __init__, __repr__ and __eq__ onto the class. Class decorators are explicit and local, which is their strength. They are also their limit: a decorator applies to one class only, and subclasses do not inherit it.
__init_subclass__: a hook for every subclass
When a class is created, Python calls __init_subclass__ on its parent. The parent can then register, check or modify each new subclass. Keyword arguments in the class line, other than metaclass, are passed to it:
class Reader: registry = {}
def __init_subclass__(cls, /, ext, **kwargs): super().__init_subclass__(**kwargs) Reader.registry[ext] = cls
@classmethod def for_file(cls, filename): ext = filename.rsplit(".", 1)[-1] return cls.registry[ext]()class CsvReader(Reader, ext="csv"): pass
class JsonReader(Reader, ext="json"): pass
print(Reader.registry)print(type(Reader.for_file("data.json")).__name__){'csv': <class '__main__.CsvReader'>, 'json': <class '__main__.JsonReader'>}JsonReaderNobody had to call a register function: defining the subclass was enough. Two details matter. __init_subclass__ is implicitly a class method, so cls is the new subclass, not Reader. And it should accept **kwargs and pass them to super().__init_subclass__, so that several base classes with hooks can be combined.
The same hook can refuse a subclass. The error then appears when the module is imported, not much later when someone uses the class:
class Command: def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) if not isinstance(cls.__dict__.get("help"), str): raise TypeError(f"{cls.__name__} must define a help string")
class Deploy(Command): help = "Deploy the site"
try: class Broken(Command): passexcept TypeError as error: print("TypeError:", error)print(Deploy.help)TypeError: Broken must define a help stringDeploy the siteThe check uses cls.__dict__, not getattr, so a subclass of Deploy would also need its own help text. Use getattr(cls, "help", None) if an inherited value is good enough.
The order of the hooks
Descriptors from the previous lesson get their __set_name__ call during the same process. Here is the full sequence for one class:
class Field: def __set_name__(self, owner, name): print(f"2. __set_name__: {owner.__name__}.{name}")
class Base: def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) print(f"3. __init_subclass__: {cls.__name__}")
def decorate(cls): print(f"4. decorator: {cls.__name__}") return cls
@decorateclass Model(Base): print("1. the class body runs") title = Field()1. the class body runs2. __set_name__: Model.title3. __init_subclass__: Model4. decorator: ModelThe body runs first, as ordinary code, and fills a namespace. Then type builds the class, calls __set_name__ on each attribute that has one, calls the parent’s __init_subclass__, and finally the decorators run, bottom-up. A decorator therefore sees a class whose fields and parent hooks are already set up.
__class_getitem__: subscripting a class
list[int] works because list defines __class_getitem__. Like __init_subclass__, it is implicitly a class method. The usual return value is a types.GenericAlias, which remembers the class and the arguments and can still be called to create instances:
from types import GenericAlias
class Box: def __init__(self, item): self.item = item
def __class_getitem__(cls, item): return GenericAlias(cls, item)
alias = Box[int]print(alias, alias.__origin__.__name__, alias.__args__)box = alias(5)print(type(box).__name__, box.item)__main__.Box[int] Box (<class 'int'>,)Box 5This hook exists for type hints, and should not be used for anything else, such as a lookup table keyed by class. types.GenericAlias was added in Python 3.9, which is why this lesson needs 3.9. For your own generic classes, write class Box[T]: as in generics with type parameter syntax, which gives you subscripting and type checker support without writing the hook yourself.
__slots__: a fixed set of attributes
A class with __slots__ lists its instance attributes up front. Python then gives instances no __dict__, which saves memory and turns typos into errors:
class Point: __slots__ = ("x", "y")
def __init__(self, x, y): self.x = x self.y = y
p = Point(1, 2)print(type(Point.__dict__["x"]).__name__)print(hasattr(p, "__dict__"))try: p.z = 3except AttributeError as error: print("AttributeError:", error)member_descriptorFalseAttributeError: 'Point' object has no attribute 'z' and no __dict__ for setting new attributesmember_descriptor is the clue to how it works: each slot is a data descriptor on the class, which reads and writes a fixed position in the instance. You met the convenient form, @dataclass(slots=True), in dataclasses. Because there is no instance dictionary, functools.cached_property and the lazy descriptor from the last lesson cannot store anything on slotted instances.
Requires version 3.15 or later A subclass of tuple, including one created by collections.namedtuple, can now add non-empty __slots__. Earlier versions raise TypeError: nonempty __slots__ not supported for subtype of 'tuple':
class Pair(tuple): __slots__ = ("label",)
def __new__(cls, a, b, label): self = super().__new__(cls, (a, b)) self.label = label return self
pair = Pair(1, 2, "origin")print(pair, pair.label, len(pair))(1, 2) origin 2Python 3.15 also allows "__dict__" and "__weakref__" in __slots__ for any class, including subclasses of int and other built-ins.
When not to use these hooks
Every hook in this lesson runs at import time and acts at a distance: a reader of class CsvReader(Reader, ext="csv") has to find Reader to learn what happens. Prefer the most visible tool that does the job. A plain function call such as registry.add(CsvReader) is easy to follow and to test. A class decorator is visible at the definition. Reach for __init_subclass__ when every subclass must be handled the same way and forgetting would be a bug.
Common pitfalls
- Not calling
super().__init_subclass__(**kwargs). The next class in the method resolution order never gets its hook, and unknown keywords are silently lost. - Registering the base class.
__init_subclass__runs for subclasses only, which is usually what you want. Do not expect it to fire for the class that defines it. - Using
__class_getitem__as a general lookup. It is meant for type hints. Use a dictionary or a class method for anything else. - Defaults as class attributes next to
__slots__.x = 0in the class body clashes with a slot namedxand raisesValueError. Set defaults in__init__.
Write a base class Command with a dictionary commands. Each subclass registers itself under the name given in its class line, or under its class name in lower case when there is none. Put it in command_registry.py. With your class, this code:
from command_registry import Command
class Hello(Command): def run(self): return "hello!"
class Bye(Command, name="exit"): def run(self): return "goodbye"
print(sorted(Command.commands))print(Command.commands["hello"]().run())print(Command.commands["exit"]().run())prints:
['exit', 'hello']hello!goodbyeShow solution
class Command: commands = {}
def __init_subclass__(cls, /, name=None, **kwargs): super().__init_subclass__(**kwargs) Command.commands[name or cls.__name__.lower()] = clsWith this file next to it, commands_check.py prints the three expected lines. name=None makes the keyword optional, and or falls back to the class name. Writing Command.commands rather than cls.commands makes it explicit that every subclass shares one dictionary, even if a subclass later defines its own commands attribute.
Job should be both registered and audited, but the audit line never appears:
class Registered: names = []
def __init_subclass__(cls): Registered.names.append(cls.__name__)
class Audited: def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) print("audited:", cls.__name__)
class Job(Registered, Audited): pass
print(Registered.names)['Job']Fix it so that the output is:
audited: Job['Job']Show solution
class Registered: names = []
def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) Registered.names.append(cls.__name__)
class Audited: def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) print("audited:", cls.__name__)
class Job(Registered, Audited): pass
print(Registered.names)audited: Job['Job']Python calls __init_subclass__ once, on the next class in Job’s method resolution order, which is Registered. Its hook did not call super(), so Audited.__init_subclass__ was never reached. With super().__init_subclass__(**kwargs) the call travels along the chain. The audit line comes first because Registered appends after its super() call returns.
Write a class decorator counted that adds a class attribute created, starting at 0, and increases it each time an instance is initialised. Put it in counted.py. This code:
from counted import counted
@countedclass Job: def __init__(self, name): self.name = name
Job("a")Job("b")print(Job.created, Job("c").name, Job.created)prints 2 c 3.
Show solution
from functools import wraps
def counted(cls): original = cls.__init__
@wraps(original) def __init__(self, *args, **kwargs): original(self, *args, **kwargs) cls.created += 1
cls.created = 0 cls.__init__ = __init__ return clsWith it, counted_check.py prints 2 c 3. The decorator keeps the original __init__, installs a wrapper that calls it and then counts, and returns the same class. wraps keeps the name and docstring of the original method. Arguments are evaluated left to right, so the first Job.created is read before Job("c") runs.
Summary
- Class decorators receive the finished class and return it, often with new methods. They apply to one class, not its subclasses.
__init_subclass__runs on the parent for every new subclass and receives the class keyword arguments. Always pass**kwargstosuper().- Class creation order: body,
typebuilds the class,__set_name__,__init_subclass__, then decorators. __class_getitem__makesClass[...]work and is meant for type hints.class Box[T]is the easier route.__slots__replaces the instance dictionary with slot descriptors. Requires version 3.15 or later Tuple subclasses can add slots.- Next: metaclasses, the machinery behind all of this.