3

I have a class that inherits from ABC, and does not have any abstractmethod.

I would like to check if it's an abstract class, and am currently stumped.

Determine if a Python class is an Abstract Base Class or Concrete prescribes using inspect.isabstract. However, this only works if an abstractmethod has been used.

How can I detect if a class inherits directly from ABC, without using inspect.isabstract?


Test Cases

# I need this to be flagged as abstract
class AbstractBaseClassNoAbsMethod(ABC):
    pass


# This is currently flaggable with `inspect.isabstract`
class AbstractBaseClassWithAbsMethod(ABC):
    @abstractmethod
    def some_method(self):
        """Abstract method."""


# I need this to be flagged as not abstract
class ChildClassFromNoAbsMethod(AbstractBaseClassNoAbsMethod):
    pass

I considered using issubclass(some_class, ABC), but this is True, even for the above ChildClassFromNoAbsMethod.


Current Best Solution

My current best solution works using __bases__. This is basically just listing parent classes, see How to get the parents of a Python class?

def my_isabstract(obj) -> bool:
    """Get if ABC is in the object's __bases__ attribute."""
    try:
        return ABC in obj.__bases__
    except AttributeError:
        return False

This is a viable solution. I am not sure if there is a better/more standard way out there.

Intrastellar Explorer
  • 3,005
  • 9
  • 52
  • 119

2 Answers2

3

AbstractBaseClassNoAbsMethod isn't abstract. Inheriting from ABC doesn't make a class abstract. inspect.isabstract is producing correct results. You'll also see that no error occurs if you try to instantiate AbstractBaseClassNoAbsMethod, while attempting to instantiate an actually abstract class raises an exception.

If you want to test whether a class inherits directly from abc.ABC, you can do what you're already doing with __bases__, but many abstract classes don't inherit from abc.ABC. For example, this is an abstract class:

class Abstract(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def foo(self):
        pass

and so is B in this example:

class A(abc.ABC):
    @abc.abstractmethod
    def foo(self):
        pass

class B(A): pass

but neither Abstract nor B inherits directly from abc.ABC.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • It seems I have a misunderstanding of abstract classes – Intrastellar Explorer Jun 12 '20 at 22:14
  • Python's implementation of Abstract classes is actually quite peculiar - as the check only takes place in runtime, and when trying to instantiate an abstract class. As far as traditional OOP literature is concerned, Python does not have "abstract classes" at all. – jsbueno Jun 12 '20 at 22:18
2

In fact, even if one uses ABCMeta as a metaclass, if there are no abstractmethods (or attributes or properties), a class is not abstract for all purposes in Python, since it can be instantiated normally.

That is why inspect.isabstract will return False on it.

If you really need this check, then, checking the class __bases__ attribute for ABC is the way to go.

And even then, if one just use the metaclass ABCMeta directly this check will fail:

class MyClass(metaclass=ABCMeta):
   pass

Now, if we write some code to flag that MyClass above as "Abstract" and another one derived from it as "not abstract", as you want with your example ChildClassFromNoAbsMethod, then any class derived from ABC would likewise be the "subclass of a class that was declared with ABCMeta metaclass".

Still, it is possible to check if a class has the ABCMeta in its metaclass hierarchy, and inspect all the way up its ancestors to "see" if it either is the first class using ABCMeta as a metaclass in its hierarchy, or a direct child of abc.ABC. But as you've probably noted, it is of little use.

You'd better just have a throw away marker abstract attribute in yourAbstractBaseClassNoAbsMethod that would have to be overriden, then both inspect.iabstract and the language mechanisms to prevent instantiation would simply work:


In [37]: class A(ABC): 
    ...:     marker = abstractmethod(lambda : None) 
    ...:                                                                                                                             

In [38]: inspect.isabstract(A)                                                                                                       
Out[38]: True

In [39]: class B(A): 
    ...:     marker = None 
    ...:                                                                                                                             

In [40]: B()                                                                                                                         
Out[40]: <__main__.B at 0x7f389bf19cd0>

In [41]: inspect.isabstract(B)                                                                                                       
Out[41]: False
jsbueno
  • 99,910
  • 10
  • 151
  • 209