This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: Changing cls.__bases__ must ensure proper metaclass inheritance
Type: behavior Stage:
Components: Interpreter Core Versions: Python 3.8
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: gvanrossum Nosy List: abusalimov, gvanrossum, rhettinger, xtreak
Priority: normal Keywords:

Created on 2014-07-04 20:45 by abusalimov, last changed 2022-04-11 14:58 by admin.

Messages (6)
msg222312 - (view) Author: Eldar Abusalimov (abusalimov) * Date: 2014-07-04 20:45
When a new class is constructed Python checks for possible metaclass conflicts within bases and an explicitly specified one, if any, choosing the best available (the most specialized) one. That is the following implication is expected:

    issubclass(B, A) => issubclass(type(B), type(A))

However, changing __bases__ attribute can break this invariant silently without an error.

>>> class O(object):
...     pass
... 
>>> class M(type):
...     pass
... 
>>> class N(type):
...     pass
... 
>>> class A(O, metaclass=M):
...     pass
... 
>>> class B(O, metaclass=N):
...     pass
... 
>>> B.__bases__ = (A,)
>>> B.__mro__
(<class '__main__.B'>, <class '__main__.A'>, <class '__main__.O'>, <class 'object'>)
>>> type(B)
<class '__main__.N'>
>>> type(A)
<class '__main__.M'>
>>> issubclass(B, A)
True
>>> issubclass(type(B), type(A))
False

Trying to derive from B now makes things look pretty weird:

>>> class C(A, metaclass=N):
...     pass
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
>>> class D(B, A): pass
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
>>> class E(B, metaclass=N):
...     pass
... 
>>> type(E)
<class '__main__.N'>

That is one can extend a class but not its base (and not a class along its base). This effectively allows to bypass metaclass checks (by introducing a dummy class with the default metaclass, deriving it from a desired class with an inappropriate metaclass by changing __bases__ and using it instead of the desired class).

This behavior is observed in 2.7, 3.2 and 3.4.


I would expect the same check for metaclass conflicts when changing __bases__ as upon creating a new class:

>>> # EXPECTED:
... 
>>> B.__bases__ = (A,)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
msg236753 - (view) Author: Mark Lawrence (BreamoreBoy) * Date: 2015-02-27 14:47
@Eldar sorry that this issue slipped our net.
msg237072 - (view) Author: Eldar Abusalimov (abusalimov) * Date: 2015-03-02 18:24
@Mark, that OK, for the issue without a patch. :)

I could make a patch, but I'm not sure whether the proposed behavior is right. It could be considered arguable, I guess...
msg326532 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python committer) Date: 2018-09-27 08:10
Thanks for the report and patience. This behavior is still reproducible on master as of f55c64c632 . I am adding Raymond as part of triaging who might have a better explanation about this. Raymond, feel free to remove yourself if this is not relevant.

A slightly cleaned up version of the program with repl statements removed for reference : 

class O(object):
     pass

class M(type):
     pass

class N(type):
     pass

class A(O, metaclass=M):
     pass

class B(O, metaclass=N):
     pass

print(B.__bases__)
print(B.__mro__)

print(type(B))
print(type(A))
print(issubclass(type(B), type(A)))

class C(A, metaclass=N):
     pass

class D(B, A):
     pass

class E(B, metaclass=N):
     pass
msg326650 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2018-09-28 18:17
>  I am adding Raymond as part of triaging who might have a better explanation about this. 

Guido, is this something we care about?  There is value in checking for metaclass conflicts when a class is created (to catch plausible mistakes and to assure that the right __new__() and __init__() methods run).  But post-creation, it's unclear whether there is value in rerunning checks and whether its even possible to know how other base classes might have affected the class creation.

Do this go in the "consenting adults" category (in the same way that we allow the __class__ attribute to be changed on instances without trying to verify whether it makes sense)?
msg326651 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2018-09-28 18:34
As long as you can't crash CPython with this, this is the responsibility of the code that assigns to __bases__. If they don't take care, the program's behavior is undefined.
History
Date User Action Args
2022-04-11 14:58:05adminsetgithub: 66118
2018-09-28 18:34:40gvanrossumsetmessages: + msg326651
2018-09-28 18:17:36rhettingersetversions: + Python 3.8, - Python 2.7, Python 3.4, Python 3.5
nosy: + gvanrossum

messages: + msg326650

assignee: gvanrossum
2018-09-27 08:10:55xtreaksetnosy: + rhettinger
messages: + msg326532
2018-09-23 08:50:57BreamoreBoysetnosy: - BreamoreBoy
2018-09-23 06:08:18xtreaksetnosy: + xtreak
2015-03-02 18:24:58abusalimovsetmessages: + msg237072
2015-02-27 14:47:26BreamoreBoysetnosy: + BreamoreBoy

messages: + msg236753
versions: + Python 3.5, - Python 3.2
2014-07-04 20:45:25abusalimovcreate