__init_subclass__ – a simpler way to implement class registries in Python
• 3 min read
I overlooked that fact that Python 3.6 added
a new class method to object called __init_subclass__
.
Once use case where it shines is class registries. Put simply, this is keeping track (in a global or equivalent object) of the subclasses of a class that have been defined (so that they can be easily accessed later on for some other purpose). Previously, this would have been done using metaclasses, but now there is a much cleaner way to do it.
A simple example
_registry = []
class AbstractAnimal:
@classmethod
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
_registry.append(cls)
class Dog(AbstractAnimal):
pass
After running this, _registry
will contain the class Dog
. It’s important to
note that the code in AbstractAnimal.__init_subclass__()
runs when the module
containing a subclass such as Dog
is imported. If Dog
was in a separate
module which was never imported, it would never get added to _registry
.
(Although I defined _registry
outside of the class, you can define it as a
class attribute if you prefer.)
Customising behaviour per class using keyword arguments
You can also pass keyword arguments to the class which are then forwarded to
__init__subclass__()
. One use of this is to be able to define subclasses of
your base class that will be excluded from your registry (for example,
subclasses with abstract methods). Here’s an example:
_registry = []
class AbstractAnimal:
@classmethod
def __init_subclass__(cls, is_abstract=False, **kwargs):
super().__init_subclass__(**kwargs)
if not is_abstract:
_registry.append(cls)
class AbstractPet(AbstractAnimal, is_abstract=True):
pass
class Dog(AbstractPet):
pass
After running this, _registry
will still only contain Dog
. This is because
when AbstractAnimal.__init_subclass__()
is called for AbstractPet
, it is
passed is_abstract=True
which is then used to exclude it from the registry.
When AbstractAnimal.__init_subclass__()
is called for Dog
, it is not
passed is_abstract=True
, and hence it picks up the default of
is_abstract=False
and is added to the class registry. In other words,
subclasses do not inherit their base classes’ keyword arguments, making class
keyword arguments perfect for this use.
(As an aside, if you did want them to be inherited, you could simply use class attributes for that.)
If you wanted a class keyword argument to be required, you can simply leave off
the default value in your __init_subclass__()
definition. (Note that you must
still use the keyword argument syntax in subclasses, otherwise your argument
will be interpreted as a base class.)
What are all the super calls for?
Why is super().__init_subclass__
called, and why is is_abstract
not passed
to it? This simply ensures the correct behaviour when multiple inheritance is
used. Here’s another example:
class BaseA:
@classmethod
def __init_subclass__(cls, a_arg, **kwargs):
print("BaseA.__init_subclass__", a_arg, kwargs)
super().__init_subclass__(**kwargs)
class BaseB:
@classmethod
def __init_subclass__(cls, b_arg, **kwargs):
print("BaseB.__init_subclass__", b_arg, kwargs)
super().__init_subclass__(**kwargs)
class SubAB(BaseA, BaseB, a_arg='a', b_arg='b'):
pass
This prints:
BaseA.__init_subclass__ a {'b_arg': 'b'}
BaseB.__init_subclass__ b {}
(Of course, things will still break if two base classes use the same class keyword arguments, so you still need to be careful.)
The last call to super().__init_subclass__()
calls the default implementation
of __init_subclass__()
(which is object.__init_subclass__()
). This raises an
error if any keyword arguments are passed to it. For example, if we add another
argument to SubAB
:
class SubAB(BaseA, BaseB, a_arg='a', b_arg='b', d_arg='d'):
pass
we get an exception:
TypeError: __init_subclass__() takes no keyword arguments
So, typos and similar be picked up as long as your __init_subclass__()
is
written correctly.
Subclasses must be imported
As mentioned earlier, for __init_subclass__ ()
to be called for a particular
subclass the module containing that submodule must be imported. You can do that
using normal import
statements. However, if you want to build some kind of
auto-discovery logic or a configuration-based system, I would recommend looking
at
importlib.import_module()
.
Other uses
Although I’ve used __init_subclass__()
for class registries, its use is not
limited to that. You can manipulate cls
in any valid way in that method (e.g.
set class attributes which affect the behaviour of other methods). Nonetheless,
you will probably want to keep any logic in __init_subclass__ ()
relatively
simple, so that it doesn’t make it too difficult for others (or future you) to
understand.
A note on @classmethod
__init_subclass__()
is a class method, and as such I have decorated it with
@classmethod
. Technically, using the decorator is optional in this case, but I
would still recommend that you do use it to make it clear that it is a class
method to those less familiar with it.
Further reading
For further information on __init_subclass__()
I would recommend reading the
relevant PEP: https://www.python.org/dev/peps/pep-0487/