47

My Python application contains many abstract classes and implementations. For example:

import abc
import datetime

class MessageDisplay(object):
    __metaclass__ = abc.ABCMeta

    @abc.abstractproperty
    def display(self, message):
        pass

class FriendlyMessageDisplay(MessageDisplay):
    def greet(self):
        hour = datetime.datetime.now().timetuple().tm_hour

        if hour < 7:
            raise Exception("Cannot greet while asleep.")
        elif hour < 12:
            self.display("Good morning!")
        elif hour < 18:
            self.display("Good afternoon!")
        elif hour < 20:
            self.display("Good evening!")
        else:
            self.display("Good night.")

class FriendlyMessagePrinter(FriendlyMessageDisplay):
    def display(self, message):
        print(message)

FriendlyMessagePrinter is a concrete class that we can use...

FriendlyMessagePrinter().greet()
Good night.

...but MessageDisplay and FriendlyMessageDisplay are abstract classes and attempting to instantiate one would result in an error:

TypeError: Can't instantiate abstract class MessageDisplay with abstract methods say

How can I check if a given class object is an (uninstantiatable) abstract class?

Jeremy
  • 1
  • 85
  • 340
  • 366
  • 2
    Ref. for casual readers, ABC: http://docs.python.org/2/library/abc.html –  Jan 19 '13 at 05:17
  • Mark FriendlyMessageDisplay, and other subclasses that don't implement `display()` , as an ABC and then use `__classes__` to determine if the class is an ABC as described in this post: https://stackoverflow.com/questions/62352982/python-determine-if-class-is-abstract-abc-without-abstractmethod – lreeder Sep 21 '22 at 17:43

3 Answers3

64
import inspect
print(inspect.isabstract(object))                  # False
print(inspect.isabstract(MessageDisplay))          # True
print(inspect.isabstract(FriendlyMessageDisplay))  # True
print(inspect.isabstract(FriendlyMessagePrinter))  # False

This checks that the internal flag TPFLAGS_IS_ABSTRACT is set in the class object, so it can't be fooled as easily as your implementation:

class Fake:
    __abstractmethods__ = 'bluh'

print(is_abstract(Fake), inspect.isabstract(Fake)) # True, False
a_guest
  • 34,165
  • 12
  • 64
  • 118
mmgp
  • 18,901
  • 3
  • 53
  • 80
  • 6
    At least in Python 3.7.6, this does not seem to work unless the abstract class has at least one `@abstractmethod` (or related decorator) in it. Simply inheriting from `ABC` or using the `ABCMeta` class does not make a class abstract for this purpose. – Soren Bjornstad May 10 '20 at 20:22
10

Abstract classes and their concrete implementations have an __abstractmethods__ attribute containing the names of abstract methods and properties that have not been implemented. This behaviour is described in PEP 3199:

Implementation: The @abstractmethod decorator sets the function attribute __isabstractmethod__ to the value True. The ABCMeta.__new__ method computes the type attribute __abstractmethods__ as the set of all method names that have an __isabstractmethod__ attribute whose value is true. It does this by combining the __abstractmethods__ attributes of the base classes, adding the names of all methods in the new class dict that have a true __isabstractmethod__ attribute, and removing the names of all methods in the new class dict that don't have a true __isabstractmethod__ attribute. If the resulting __abstractmethods__ set is non-empty, the class is considered abstract, and attempts to instantiate it will raise TypeError. (If this were implemented in CPython, an internal flag Py_TPFLAGS_ABSTRACT could be used to speed up this check.)

So in concrete classes, this attribute either will not exist or will be an empty set. This is easy to check:

def is_abstract(cls):
    if not hasattr(cls, "__abstractmethods__"):
        return False # an ordinary class
    elif len(cls.__abstractmethods__) == 0:
        return False # a concrete implementation of an abstract class
    else:
        return True # an abstract class

Or more succinctly:

def is_abstract(cls):
    return bool(getattr(cls, "__abstractmethods__", False))
print(is_abstract(object))                 # False
print(is_abstract(MessageDisplay))         # True
print(is_abstract(FriendlyMessageDisplay)) # True
print(is_abstract(FriendlyMessagePrinter)) # False
Jeremy
  • 1
  • 85
  • 340
  • 366
  • Returning to explain the downvote: `from numbers import Number; from inspect import isabstract; print(isabstract(Number), Number.__abstractmethods__)` → `False, frozenset()`, despite `type(Number) is abc.ABCMeta` (that's `True`) and `Number` being non-instantiable. Your solution suffers the same problem. – amcgregor Mar 11 '20 at 19:32
  • This doesn't work if the abstract class has no abstract methods defined. In this case `__abstractmethods__` is empty. – MMM Jan 08 '23 at 10:46
-3

You could do this with the _ast module. For example, if your example code were in foo.py you could invoked this function with "foo.py" and "FriendlyMessagePrinter" as arguments.

def is_abstract(filepath, class_name):
    astnode = compile(open(filename).read(), filename, 'exec', _ast.PyCF_ONLY_AST)
    for node in astnode.body:
        if isinstance(node, _ast.ClassDef) and node.name == class_name:
            for funcdef in node.body:
                if isinstance(funcdef, _ast.FunctionDef):
                    if any(not isinstance(n, _ast.Pass) for n in funcdef.body):
                        return False
            return True
    print 'class %s not found in file %s' %(class_name, filepath)
Jeremy
  • 1
  • 85
  • 340
  • 366
inspectorG4dget
  • 110,290
  • 27
  • 149
  • 241
  • `s/filepath/filename`. But this is obviously wrong, right ? It directly fails for a class without methods, and also if I simply decide to create a function like `def bluh(self): pass`. – mmgp Jan 19 '13 at 04:12
  • 1
    Creative solution, but this would also fail on my full project because several abstract methods have implementations (invoked through `super`), not just `pass`. – Jeremy Jan 19 '13 at 05:04