Motivations
As powerful as metaclasses are in customizing class behaviors, they are also inherently prone to causing conflicts with other metaclasses, as pointed out in PEP-487 in its reasoning to add the __init_subclass__
hook to a class:
Metaclasses are a powerful tool to customize class creation. They have, however, the problem that there is no automatic way to combine metaclasses. If one wants to use two metaclasses for a class, a new metaclass combining those two needs to be created, typically manually.
and:
One of the big issues that makes library authors reluctant to use metaclasses (even when they would be appropriate) is the risk of metaclass conflicts. These occur whenever two unrelated metaclasses are used by the desired parents of a class definition. This risk also makes it very difficult to add a metaclass to a class that has previously been published without one.
Currently the biggest use of a metaclass in the standard library is by far abstract classes with abc.ABC
and the deriving collections.abc
classes. And yet, there are countless times when one follows good OOD principles and makes a main class inherit from one or more of those abstract classes to signify certain characteristics of the main class, only to end up causing unexpected metaclass conflicts and having to resort to an ugly manual metaclass merge, if a merge can be done at all (a merge cannot be done if there are methods in both metaclasses that don’t cooperate with others by calling super()
).
In the following StackOverflow question, the OP can be seen wondering why a class inheriting from configparser.ConfigPaser
and PyQt5.QtGui.QStandardItem
causes a metaclass conflict error:
from PyQt5.QtGui import QStandardItem
from configparser import ConfigParser
class FinalClass(ConfigParser, QStandardItem):
...
As it turns out, this is because as it is currently coded, the parent class of ConfigPaser
inherits from collections.abc.MutableMapping
, making its metaclass different from that of PyQt5.QtGui.QStandardItem
, whose developer chooses to use a metaclass to implement a proxy class behavior.
This is really unfortunate since the risk of a metaclass conflict effectively discourages developers from using abstract classes even when it makes total sense to do so, affecting both code reusability and readability in the long run.
Obviously this is an issue already widely acknowledged as mentioned in PEP-487, with its introduction of __init_subclass__
and __set_name__
a giant leap towards elimination of unnecessary use of metaclasses.
But even with PEP-487, abc.ABC
remains as the big elephant standing in the room with its metaclass abc.ABCMeta
. Reading the code of abc.ABCMeta
, it is apparent that the only reason why this metaclass exists is to include __instancecheck__
and __subclasscheck__
methods in order to support isinstace
and issubclass
calls with user-registered virtual subclasses, and per the documentation, the two dunder methods are looked up on the metaclass only:
Note that these methods are looked up on the type (metaclass) of a class. They cannot be defined as class methods in the actual class. This is consistent with the lookup of special methods that are called on instances, only in this case the instance is itself a class.
So the solution now becomes clear…
The Proposal
Make __instancecheck__
and __subclasscheck__
actual class methods, such that when isinstance
and issubclass
are called, these methods are looked up directly on the class object of the second argument itself (in addition to its type, for backward compatibility).
Existing metaclass-powered classes in the standard library, such as abc.ABC
and typing.Protocol
, should be refactored as regular classes.
Note that back in CPython 3.11, there was a comment in typing._ProtocolMeta
that states:
This metaclass is really unfortunate and exists only because of the lack of
__instancehook__
.
But since CPython 3.12, the wording of that comment has changed to state:
This metaclass is somewhat unfortunate, but is necessary for several reasons…
along with an addition of an overriding __new__
method, which, as far as I can tell, is all about fixing an issue about determining when to raise an exception from trying to mix a protocol with a non-protocol class, and could’ve been done entirely with __init_subclass__
instead.
Backward Compatibility
There should be no backward compatibility issues since __instancecheck__
and __subclasscheck__
currently have no effect when defined in a regular class, so there should be no existing code base with a regular class with such dunders defined.
Performance Impact
Since an additional attribute lookup on the class object needs to be performed to support this proposal, there will be a small performance impact to calls to isinstance
and issubclass
. But the huge gains in code reusability and readability should be worth the tradeoff.