Why does defining new class sometimes call the __init__() function of objects that the class inherits from?

Question:

I’m trying to understand what actually happens when you declare a new class which inherits from a parent class in python.

Here’s a very simple code snippet:

# inheritance.py
class Foo():
    def __init__(self, *args, **kwargs):
        print("Inside foo.__init__")
        print(args)
        print(kwargs)


class Bar(Foo):
    pass

print("complete")

If I run this there are no errors and the output is as I would expect.

❯ python inheritance.py                                                                                                                                                                               
complete

Here’s a script with an obvious bug in it, I inherit from an instance of Foo() rather than the class Foo

# inheritance.py
class Foo():
    def __init__(self, *args, **kwargs):
        print("Inside foo.__init__")
        print(f"{args=}")
        print(f"{kwargs=}n")

foo = Foo()
class Bar(foo):            <---- This is wrong
    pass

print("complete")

This code runs without crashing however I don’t understand why Foo.__init__() is called twice.

Here’s the output:

❯ python inheritance.py
Inside foo.__init__        <--- this one I expected
args=()
kwargs={}

Inside foo.__init__       <--- What is going on here...?
args=('Bar', (<__main__.Foo object at 0x10f190b10>,), {'__module__': '__main__', '__qualname__': 'Bar'})
kwargs={}

complete

On line 8 I instantiate Foo() with no arguments which is what I expected. However on line 9 Foo.__init__ is called with the arguments that would normally be passed to type() to generate a new class.

I can see vaguely what’s happening: class Bar(...) is code that generates a new class so at some point type("Bar", ...) needs to be called but:

  1. How does this actually happen?
  2. Why does inheriting from an instance of Foo() cause Foo.__init__("Bar", <tuple>, <dict>) to be called?
  3. Why isn’t type("Bar", <tuple>, <dict>) called instead?
Asked By: David McLean

||

Answers:

Python is using foo to determine the metaclass of Bar. No explicit metaclass is given, so the "most derived metaclass" must be determined. The metaclass of a base class is its type; usually, that’s type itself. But in this case, the type of the only base "class", foo, is Foo, so that becomes the most derived metaclass. And so,

class Bar(foo):
    pass

is being treated as

class Bar(metaclass=Foo):
    pass

which means that Bar is created by calling Foo:

Bar = Foo('Bar', (foo,), {})

Note that Bar is now an instance of Foo, not a type. Yes, a class statement does not necessarily create a class.

Answered By: chepner