Subclasshook and other Python ABCs

Python’s blend of dynamic features and “static-friendly” elements like Abstract Base Classes (ABCs) and type hints is quite powerful. It allows the language to remain flexible while providing tools for those who prefer a more static approach. One intriguing mechanism is the ability to influence how Python checks for class inheritance (using the issubclass function) and instance relationships (using the isinstance function).

When you use issubclass(Student, Person) or isinstance(p1, Person), Python searches for __subclasscheck__ and __instancecheck__ methods, respectively. These methods, however, aren’t found directly within the Person class. Instead, they’re looked up in the class’s metaclass. Typically, this means the type metaclass is used unless a custom metaclass is in play. The type class is implemented in C, so these checks ultimately boil down to functions within the CPython interpreter.

The purpose of __isinstance__ and __issubclass__ is to expand the scenarios where an object passes isinstance and issubclass checks. They are not designed for restriction — that is, making objects that would normally pass these checks fail. Attempting to use them for restriction can lead to inconsistent behavior. While it might seem to work for issubclass checks, it can produce unexpected results with isinstance due to an optimization.

here details an optimization within isinstance: if type(obj) is equal to the given class, it immediately returns True.

The code snippet below demonstrates the unpredictable outcomes when attempting to use these mechanisms for restriction. Note how the optimization in isinstance causes discrepancies:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
 `# https://stackoverflow.com/questions/52168971/instancecheck-overwrite-shows-no-effect-what-am-i-doing-wrong

class Meta1(type):
    def __instancecheck__(cls, inst):
        """Implement isinstance(inst, cls)."""
        print("__instancecheck__")
        #return super().__instancecheck__(inst)
        return False
    
    def __subclasscheck__(cls, sub):
        """Implement issubclass(sub, cls)."""
        print("__subclasscheck__")
        #return super().__subclasscheck__(sub)
        return False
    

class Person1(metaclass=Meta1):
    pass

class Worker1(Person1):
    pass


p1 = Person1()
print(isinstance(p1, Person1))
# True
# it uses the optimization and our custom __isinstance__ is not even invoked
# type(p1) 

print("------------------------")

p1 = Worker1()
print(isinstance(p1, Person1))
# False
# the optimization returns False, so it invokes our custom __isinstance__ that returns False

print("------------------------")

print(issubclass(Person1, Person1))
# False
# __subclasscheck__ is invoked and returns False

print("------------------------")

print(issubclass(Worker1, Person1))
# False
# __subclasscheck__ is invoked and returns False
print("------------------------")` 

A key reason for introducing this machinery was to empower abstract classes. It simplifies the process of performing what you might call “multiple duck-typing checks.” Imagine needing an object that adheres to an interface defined by an ABC, but without requiring direct inheritance — you only care about the presence of certain methods (the essence of duck typing). The __subclasshook__ method comes into play here. By defining a @classmethod def __subclasshook__(cls, subclass) within your ABC, you can control this behavior. Python’s built-in ABCs utilize ABCMeta as their metaclass, and both __isinstance__ and __issubclass__ within ABCMeta are designed to invoke the abstract class’s __subclasshook__ if it exists. This logic is implemented in the _py_abc.py file.

Let’s illustrate this with an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
 `from abc import ABC, abstractmethod

class Movable(ABC):
    @abstractmethod
    def move_rigth(self, x):
        pass

    @abstractmethod
    def move_left(self, x):
        pass

    @classmethod
    def __subclasshook__(cls, C):
        # Both isinstance() and issubclass() checks will call into this (through ABCMeta.__instancecheck__, ABCMeta.__subclasscheck__ )
        if cls is Movable:
            # 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.
            return hasattr(C, "move_right") and hasattr(C, "move_left")
        return NotImplemented


class Button:
    def move_right(self, x):
        print("moving rigth")

    def move_left(self, x):
        print("moving left")

# Button is a Movable in duck-typing and structural typing terms
# but it does not strictly implement the Movable "interface" (it does not inherit from Movable)
# thanks to the __subclasshook__ both checks below are true

if issubclass(Button, Movable):
    bt = Button()
    bt.move_right(4)
    bt.move_left(2)


bt = Button()
if isinstance(bt, Movable):
    bt.move_right(4)
    bt.move_left(2)


#moving rigth
#moving left
#moving rigth
#moving left` 

This discussion likely provides a more comprehensive explanation of these concepts.

As a side note, there’s an alternative or complementary approach to declaring compliance with an ABC’s interface: the ABCMeta.register method. Unlike __subclasshook__, where you define the conditions for compliance, register lets you explicitly state that specific classes adhere to the interface. The collections.abc module showcases interesting use cases for these mechanisms.

Licensed under CC BY-NC-SA 4.0
Last updated on Apr 03, 2023 22:32 +0100