Search code examples
pythonexceptionabstract-classpython-3.4abc

Why am I able to instantiate my Abstract Base Class in Python?


As I understand it, I can use the abc module in Python to create abstract classes that can't be instantiated (amongst other nice properties). I tried to use this to create a hierarchy of Exception classes to represent various exit codes for my application, but I'm still able to instantiate my base class, even though I don't want that to happen. Here's some code that demonstrates the problem:

#!/usr/bin/env python3

import abc

class ExitCodeException(Exception):
    __metaclass__ = abc.ABCMeta

    def __init__(self, message):
        super().__init__()
        self._message = message

    @abc.abstractmethod
    def getExitCode(self):
        """Return the exit code for this exception"""
        return

class FatalException(ExitCodeException):
    def getExitCode(self):
        return 1

raise ExitCodeException("Oh no!")

I was expecting my program to quit with an exception saying that ExitCodeException couldn't be instantiated, but instead I just get the standard stack trace I'd expect if ExitCodeException weren't abstract:

Traceback (most recent call last)
  File "./email2pdf_classexception", line 21, in <module>
    raise ExitCodeException("Oh no!")
__main__.ExitCodeException

How can I fix this?


Solution

  • As discussed in the comments by @BartoszKP and @Debanshu Kundu above, it appears the concrete superclass Exception is what causes the issue here. As such, I've come up with a slightly different pattern which seems to work (as I understand it, this is an older-style of pattern from Python 2, but still seems valid):

    #!/usr/bin/env python3
    
    class ExitCodeException(Exception):
        def __new__(cls, *args, **kwargs):
            if cls is ExitCodeException:
                raise NotImplementedError("Base class may not be instantiated")
            return Exception.__new__(cls, *args, **kwargs)
    
        def __init__(self, message):
            super().__init__()
            self._message = message
    
        def getExitCode(self):
            """Return the exit code for this exception"""
            return
    
    class FatalException(ExitCodeException):
        def getExitCode(self):
            return 1
    
    raise FatalException("Oh no!")
    

    This works as intended; if I change the code to instantiate ExitCodeException directly, it fails.