Understanding __init_subclass__

Viewed 52201

I finally upgraded my python version and I was discovering the new features added. Among other things, I was scratching my head around the new __init_subclass__ method. From the docs:

This method is called whenever the containing class is subclassed. cls is then the new subclass. If defined as a normal instance method, this method is implicitly converted to a class method.

So I started to playing around with it a little bit, following the example in the docs:

class Philosopher:
    def __init_subclass__(cls, default_name, **kwargs):
        super().__init_subclass__(**kwargs)
        print(f"Called __init_subclass({cls}, {default_name})")
        cls.default_name = default_name

class AustralianPhilosopher(Philosopher, default_name="Bruce"):
    pass

class GermanPhilosopher(Philosopher, default_name="Nietzsche"):
    default_name = "Hegel"
    print("Set name to Hegel")

Bruce = AustralianPhilosopher()
Mistery = GermanPhilosopher()
print(Bruce.default_name)
print(Mistery.default_name)

Produces this output:

Called __init_subclass(<class '__main__.AustralianPhilosopher'>, 'Bruce')
'Set name to Hegel'
Called __init_subclass(<class '__main__.GermanPhilosopher'>, 'Nietzsche')
'Bruce'
'Nietzsche'

I understand that this method is called after the subclass definition, but my questions are particularly about the usage of this feature. I read the PEP 487 article as well, but didn't help me much. Where would this method be helpful? Is it for:

  • the superclass to register the subclasses upon creation?
  • forcing the subclass to set a field at definition time?

Also, do I need to understand the __set_name__ to fully comprehend its usage?

5 Answers

You can also use it to perform once-only expensive initializations on a class.

For example, I want to replace paths starting with my user with the standard tilde shorthand for home.

/Users/myuser/.profile -> ~/.profile.

Simple, I can write this:


from pathlib import Path

class Replacer:
    def __init__(self):
        self.home = str(Path("~").expanduser())

    def replace(self, value):
        if isinstance(value,str) and value.startswith(self.home):
            value = value.replace(self.home,"~")
        return value

replacer = Replacer()
print(replacer.replace("/Users/myuser/.profile"))

But, for any run the home path is constant and there is no need to compute it every time a Replacer gets created.

Using __init_subclass, I can do it only once for the class. Yes, I could also assign the variable to the class at module initialization time:

class Replacer:

    home = str(Path("~").expanduser())
    ...

b̵u̵t̵ ̵t̵h̵e̵r̵e̵ ̵m̵a̵y̵ ̵b̵e̵ ̵r̵e̵a̵s̵o̵n̵s̵ ̵t̵o̵ ̵w̵a̵n̵t̵ ̵t̵o̵ ̵d̵e̵f̵e̵r̵ ̵t̵h̵a̵t̵ ̵c̵o̵m̵p̵u̵t̵a̵t̵i̵o̵n̵ ̵u̵n̵t̵i̵l̵ ̵t̵h̵e̵ ̵c̵l̵a̵s̵s̵ ̵a̵c̵t̵u̵a̵l̵l̵y̵ ̵g̵e̵t̵s̵ ̵u̵s̵e̵d̵ ̵f̵o̵r̵ ̵t̵h̵e̵ ̵f̵i̵r̵s̵t̵ ̵t̵i̵m̵e̵.̵ ̵ ̵F̵o̵r̵ ̵e̵x̵a̵m̵p̵l̵e̵,̵ ̵w̵h̵e̵n̵ ̵w̵o̵r̵k̵i̵n̵g̵ ̵w̵i̵t̵h̵ ̵D̵j̵a̵n̵g̵o̵,̵ ̵u̵n̵d̵e̵r̵ ̵s̵o̵m̵e̵ ̵c̵o̵n̵d̵i̵t̵i̵o̵n̵s̵ ̵w̵h̵e̵n̵ ̵y̵o̵u̵ ̵i̵m̵p̵o̵r̵t̵ ̵̵m̵o̵d̵e̵l̵s̵.̵p̵y̵̵,̵ ̵D̵j̵a̵n̵g̵o̵ ̵m̵a̵y̵ ̵n̵o̵t̵ ̵h̵a̵v̵e̵ ̵f̵u̵l̵l̵y̵ ̵i̵n̵i̵t̵i̵a̵l̵i̵z̵e̵d̵ ̵i̵t̵s̵e̵l̵f̵ ̵y̵e̵t̵.

The above is incorrect. The init subclass is executed at class definition, not first use.

However, unlike the home = assignment in the class body, the Replacer class actually exists when init subclass is called and is provided as the cls argument to that method.

class UselessAncestorNeededToHouseInitSubclass:
    "do-nothing"

    def __init_subclass__(cls, /, **kwargs):
        print("__init_subclass__")
        super().__init_subclass__(**kwargs)
        cls.home = str(Path("~").expanduser())

class Replacer(UselessAncestorNeededToHouseInitSubclass):
    """__init_subclass__ wont work if defined here.  It has to be on
    an ancestor
    """

    def replace(self, value):
        if isinstance(value,str) and value.startswith(self.home):
            value = value.replace(self.home,"~")
        return value

for ix in range(0,10):
    replacer = Replacer()
    print(replacer.replace("/Users/myuser/.profile"))

Output: (notice how subclass_init only gets called once):

__init_subclass__
~/.profile
~/.profile
~/.profile
~/.profile
~/.profile
~/.profile
~/.profile
~/.profile
~/.profile
~/.profile
Related