4

Today I have discovered that python object without __mro_entries__ can be used as a base class.

Example:

class Base:
    def __init__(self, *args):
        self.args = args

    def __repr__(self):
        return f'{type(self).__name__}(*{self.args!r})'


class Delivered(Base):
    pass


b = Base()
d = Delivered()


class Foo(b, d):
    pass


print(type(Foo) is Delivered)
print(Foo)
True
Delivered(*('Foo', (Base(*()), Delivered(*())), {'__module__': '__main__', '__qualname__': 'Foo'}))

As a result Foo will be instance of a Delivered class and it's not a valid type.

I do understand use case of __mro_entries__ but what use case of using object without __mro_entries__ as a base class. Is it a bug at python?

Yurii Karabas
  • 543
  • 4
  • 10
  • `Foo` is not just an instance of `Delivered`. It's an instance of `d`, which is an instance of `Delivered`. This is neat. – Mad Physicist Aug 11 '21 at 13:36

1 Answers1

5

TL;DR Not a bug, but an extreme abuse of the class statement.


A class statement is equivalent to a call to a metaclass. Lacking an explicit metaclass keyword argument, the metaclass has to be inferred from the base class(es). Here, the "metaclass" of the "class" b is Base, while the metaclass of d is Delivered. Since each is a non-strict subclass of a common metaclass (Base), Delivered is chosen as the more specific metaclass.

>>> Delivered('Foo', (b, d), {})
Delivered(*('Foo', (Base(*()), Delivered(*())), {}))

Delivered can be used as a metaclass because it accepts the same arguments that the class statement expects a metaclass to accept: a string for the name of the type, a sequence of parent classes, and a mapping to use as the namespace. In this case, Delivered doesn't use them to create a type; it simply prints the arguments.

As a result, Foo is bound to an instance of Delivered, not a type. So Foo is a class only in the sense that it was produced by a class statement: it is decidedly not a type.

>>> issubclass(Foo, Delivered)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: issubclass() arg 1 must be a class
>>> Foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'Delivered' object is not callable
chepner
  • 497,756
  • 71
  • 530
  • 681
  • Thanks for answer. When we create class it actually call `__build_class__` function to create class and it's behave differently if compare to `type` call. So I think it should not be the same as `type` result because any callable object can be used as a metaclass not only `type` and delivered from `type` classes. – Yurii Karabas Aug 11 '21 at 13:54
  • Right; I will update the answer in moment to address that more directly, rather than meandering into the explanation the way it currently does. (I forgot about metaclass inference when I wrote the original answer.) – chepner Aug 11 '21 at 13:58
  • I was actually unaware of the existence of `__build_class__` as the connection between a `class` statement and a call to a metaclass. I've added a link to a another question that explains it, rather than duplicating that part here. – chepner Aug 11 '21 at 14:03