python subclasscheck & subclasshook

blue_note picture blue_note · Nov 23, 2016 · Viewed 10.6k times · Source

The methods __subclasscheck__ and __subclasshook__ are used to determine if a class is regarded as subclass of another. However, their documentation is very limited, even in advanced Python books. How are they meant to be used and what is their difference (higher priority, side of relationship they refer to etc...)?

Answer

MSeifert picture MSeifert · Jan 21, 2018

Both methods can be used to customize the result of the issubclass() built-in function.

__subclasscheck__

class.__subclasscheck__(self, subclass)

Return true if subclass should be considered a (direct or indirect) subclass of class. If defined, called to implement issubclass(subclass, class).

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.

This method is the special method that is responsible for the customization of the issubclass check. Like the "Note" states it has to implemented on the metaclass!

class YouWontFindSubclasses(type):
    def __subclasscheck__(cls, subclass):
        print(cls, subclass)
        return False

class MyCls(metaclass=YouWontFindSubclasses):
    pass

class MySubCls(MyCls):
    pass

This implementation will return False even if you have genuine subclasses:

>>> issubclass(MySubCls, MyCls)
<class '__main__.MyCls'> <class '__main__.MySubCls'>
False

There are actually more interesting uses for __subclasscheck__ implementations. For example:

class SpecialSubs(type):
    def __subclasscheck__(cls, subclass):
        required_attrs = getattr(cls, '_required_attrs', [])
        for attr in required_attrs:
            if any(attr in sub.__dict__ for sub in subclass.__mro__):
                continue
            return False
        return True

class MyCls(metaclass=SpecialSubs):
    _required_attrs = ['__len__', '__iter__']

With this implementation any class that defines __len__ and __iter__ will return True in a issubclass check:

>>> issubclass(int, MyCls)  # ints have no __len__ or __iter__
False
>>> issubclass(list, MyCls)  # but lists and dicts have
True
>>> issubclass(dict, MyCls)
True

In these examples I haven't called the superclasses __subclasscheck__ and thereby disabled the normal issubclass behavior (which is implemented by type.__subclasscheck__). But it's important to know that you can also choose to just extend the normal behavior instead of completely overriding it:

class Meta(type):
    def __subclasscheck__(cls, subclass):
        """Just modify the behavior for classes that aren't genuine subclasses."""
        if super().__subclasscheck__(subclass):
            return True
        else:
            # Not a normal subclass, implement some customization here.

__subclasshook__

__subclasshook__(subclass)

(Must be defined as a class method.)

Check whether subclass is considered a subclass of this ABC. This means that you can customize the behavior of issubclass further without the need to call register() on every class you want to consider a subclass of the ABC. (This class method is called from the __subclasscheck__() method of the ABC.)

This method should return True, False or NotImplemented. If it returns True, the subclass is considered a subclass of this ABC. If it returns False, the subclass is not considered a subclass of this ABC, even if it would normally be one. If it returns NotImplemented, the subclass check is continued with the usual mechanism.

The important bit here is that it's defined as classmethod on the class and it's called by abc.ABC.__subclasscheck__. So you can only use it if you're dealing with classes that have an ABCMeta metaclass:

import abc

class MyClsABC(abc.ABC):
    @classmethod
    def __subclasshook__(cls, subclass):
        print('in subclasshook')
        return True

class MyClsNoABC(object):
    @classmethod
    def __subclasshook__(cls, subclass):
        print('in subclasshook')
        return True

This will only go into the __subclasshook__ of the first:

>>> issubclass(int, MyClsABC)
in subclasshook
True

>>> issubclass(int, MyClsNoABC)
False

Note that subsequent issubclass calls don't go into the __subclasshook__ anymore because ABCMeta caches the result:

>>> issubclass(int, MyClsABC)
True

Note that you generally check if the first argument is the class itself. That's to avoid that subclasses "inherit" the __subclasshook__ instead of using normal subclass-determination.

For example (from the CPython collections.abc module):

from abc import ABCMeta, abstractmethod

def _check_methods(C, *methods):
    mro = C.__mro__
    for method in methods:
        for B in mro:
            if method in B.__dict__:
                if B.__dict__[method] is None:
                    return NotImplemented
                break
        else:
            return NotImplemented
    return True

class Hashable(metaclass=ABCMeta):

    __slots__ = ()

    @abstractmethod
    def __hash__(self):
        return 0

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Hashable:
            return _check_methods(C, "__hash__")
        return NotImplemented

So if you check if something is a subclass of Hashable it will use the custom __subclasshook__ implementation that is guarded by the if cls is Hashable. However if you have an actual class implementing the Hashable interface you don't want it to inherit the __subclasshook__ mechanism but you want the normal subclass mechanism.

For example:

class MyHashable(Hashable):
    def __hash__(self):
        return 10

>>> issubclass(int, MyHashable)
False

Even though int implements __hash__ and the __subclasshook__ checks for an __hash__ implementation the result in this case is False. It still enters the __subclasshook__ of Hashable but it immediately returns NotImplemented which signals to ABCMeta that it should proceed using the normal implementation. If it didn't have the if cls is Hashable then issubclass(int, MyHashable) would return True!

When should you use __subclasscheck__ and when __subclasshook__?

It really depends. __subclasshook__ can be implemented on the class instead of the metaclass, but requires that you use ABCMeta (or a subclass of ABCMeta) as metaclass because the __subclasshook__ method is actually just a convention introduced by Pythons abc module.

You can always use __subclasscheck__ but it has to be implemented on the metaclass.

In practice you use __subclasshook__ if you implement interfaces (because these normally use abc) and want to customize the subclass mechanism. And you use __subclasscheck__ if you want to invent your own conventions (like abc did). So in 99.99% of the normal (not fun) cases you only need __subclasshook__.