What does it mean for a class to inherit from an object which is not a type?

Viewed 80

I encoutered a weird case of class inheritance in Python 3.8.

I can create a class by inheriting from an instance which is not itself a type. I expected a TypeError in that case, but that is not what happens.

class B:
    def __new__(cls, *args, **kwargs):
        print(args, kwargs)
        return super().__new__(cls)

b = B()  # prints: () {}

class A(b):  # I expected a TypeError here
    x = "foo"
# prints: ('A', (<__main__.B object at 0x7f32bd102390>,),
#         {'__module__': '__main__', '__qualname__': 'A', 'x': 'foo'}) {}

print(A)  # <__main__.B object at 0x7f20556c93d0>
print(isinstance(A, B))  # True
print(isinstance(A, type))  # False

It looks like when I give B() as base for A, then B is being used as the metaclass.

Is my understanding correct? Why is that the case? And I cannot seem to find anything regarding this behaviour, is it documented or is it an implementation quirk specific to cPython?

1 Answers

Basically, the type constructor will check the bases to guess the most derived metaclass. It will then see the instance of A, and use its class as the "most derived metaclass". So, yes, the class of things that go in "bases" is used as the metaclass.

From that point, it will call the retrieved "metaclass" to build the B class itself. As it is a class, it is called, this triggers the class' __new__ as usual. Since it returns an instance of "A" that is what you get as being your "B": Python does not get in the way to verify if whatever the metaclass callable returned is itself a "class" - it can be any object. This gives one the flexibility to abuse the class statement to build other kind of objects, given appropriate super-classes or metaclasses. In this case, it just happened to not raise a TypeError and return you an instance of "A". (but you had to adjust the class' __new__ to avoid the TypeError)


In [16]: class A: 
    ...:     def __new__(cls, *args, **kw): 
    ...:         print(args, kw) 
    ...:         return  super().__new__(cls) 
    ...:          
    ...:                

In [17]: class B(A()): pass                     
() {}
('B', (<__main__.A object at 0x7f26862395b0>,), {'__module__': '__main__', '__qualname__': 'B'}) {}

In [18]: type(B)        
Out[18]: __main__.A

In [19]: isinstance(B, A)                       
Out[19]: True
Related