Preventing a class from direct instantiation in Python

Question:

I have a super class with a method that calls other methods that are only defined in its sub classes. That’s why, when I create an instance of my super class and call its method, it cannot find the method and raises an error.

Here is an example:

class SuperClass(object):

  def method_one(self):
    value = self.subclass_method()
    print value


class SubClassOne(SuperClass):

  def subclass_method(self):
    return 'subclass 1'


class SubClassTwo(SuperClass):

  def subclass_method(self):
    return 'nubclass 2'


s1 = SubClassOne()
s1.method_one()

s2 = SubClassTwo()
s2.method_one()

c = SuperClass()
c.method_one()

# Results:
# subclass 1
# nubclass 2
# Traceback (most recent call last):
#   File "abst.py", line 28, in <module>
#     c.method_one()
#   File "abst.py", line 4, in method_one
#     value = self.subclass_method()
# AttributeError: 'SuperClass' object has no attribute 'subclass_method'

I was thinking about changing the __init__ of super class and verify the type of object, when a new instance is created. If the object belongs to super class raise an error. However, I’m not too sure if it’s the Pythonic way of doing it.

Any recommendations?

Asked By: mohi666

||

Answers:

You’re talking about Abstract Base Classes, and the Python language does not support them natively.

However, in the standard library, there is a module you can use to help you along. Check out the abc documentation.

Answered By: rossipedia

Your approach is a typical framework pattern.

Using __init__ to verify that type(self) is not SuperClass is a reasonable way to make sure the SuperClass hasn’t been instantiated directly.

The other common approach is to provide stub methods that raise NotImplementedError when called. That is more reliable because it also validates that subclasses have overridden the expected methods.

Answered By: Raymond Hettinger

This is what I might do:

class SuperClass(object):
    def __init__(self):
        if type(self) == SuperClass:
            raise Exception("<SuperClass> must be subclassed.")
        # assert(type(self) == SuperClass)

class SubClass(SuperClass):
    def __init__(self):
        SuperClass.__init__(self)

subC = SubClassOne()
supC = SuperClass() # This line should throw an exception

When run (exception is thrown!):

[ 18:32 jon@hozbox ~/so/python ]$ ./preventing-direct-instantiation.py
Traceback (most recent call last):
  File "./preventing-direct-instantiation.py", line 15, in <module>
    supC = SuperClass()
  File "./preventing-direct-instantiation.py", line 7, in __init__
    raise Exception("<SuperClass> must be subclassed.")
Exception: <SuperClass> must be subclassed.

Edit (from comments):

[ 20:13 jon@hozbox ~/SO/python ]$ cat preventing-direct-instantiation.py 
#!/usr/bin/python

class SuperClass(object):
    def __init__(self):
        if type(self) == SuperClass:
            raise Exception("<SuperClass> must be subclassed.")

class SubClassOne(SuperClass):
    def __init__(self):
        SuperClass.__init__(self)

class SubSubClass(SubClassOne):
    def __init__(self):
        SubClassOne.__init__(self)

class SubClassTwo(SubClassOne, SuperClass):
    def __init__(self):
        SubClassOne.__init__(self)
        SuperClass.__init__(self)

subC = SubClassOne()

try:
    supC = SuperClass()
except Exception, e:
    print "FAILED: supC = SuperClass() - %s" % e
else:
    print "SUCCESS: supC = SuperClass()"

try:
    subSubC = SubSubClass()
except Exception, e:
    print "FAILED: subSubC = SubSubClass() - %s" % e
else:
    print "SUCCESS: subSubC = SubSubClass()"

try:
    subC2 = SubClassTwo()
except Exception, e:
    print "FAILED: subC2 = SubClassTwo() - %s" % e
else:
    print "SUCCESS: subC2 = SubClassTwo()"

Prints:

[ 20:12 jon@hozbox ~/SO/python ]$ ./preventing-direct-instantiation.py 
FAILED: supC = SuperClass() - <SuperClass> must be subclassed.
SUCCESS: subSubC = SubSubClass()
SUCCESS: subC2 = SubClassTwo()
Answered By: chown

I would override __new__() in the base class and simply fail to instantiate at all if it’s the base class.

class BaseClass:    # Py3

    def __new__(cls, *args, **kwargs):
        if cls is BaseClass:
            raise TypeError(f"only children of '{cls.__name__}' may be instantiated")
        return object.__new__(cls, *args, **kwargs)

This separates concerns a little better than having it in __init__(), and "fails fast."

Answered By: kindall