Recommended way of subclassing a class while maintaining backwards compatibility

Viewed 42

Here is the scenario. I have a class Foo that looks like this:

class Foo:
    def __init__(self, prop):
        self.prop = prop

    def method(self):
        if self.prop == PROP_VAL_1:
            do_something()
        elif self.prop == PROP_VAL_2:
            do_something_2()
        else:
            raise ValueError(f"Invalid value {self.prop}")

I want to do a refactor in which I end up with two subclasses of Foo like this:

class FooA(Foo):
    def method():
        do_something()

class FooB(Foo):
    def method():
        do_something_2()

However, the issue is that I can't change how Foo is used because there is code currently in use that uses Foo. Is it possible to somehow have the superclass Foo instantiate objects of type FooA and FooB depending on the value of prop? Another thing I want to avoid is having Foo import it's subclasses since this would lead to circular dependencies. Is there some sort of "best practice" for this as well?

2 Answers

Rename the Foo class, then replace it with a factory function.

class NewFoo:
    def __init__(self, prop):
        self.prop = prop

class FooA(NewFoo):
    def method():
        do_something()

class FooB(NewFoo):
    def method():
        do_something_2()

def Foo(prop):
    if prop == PROP_VAL_1:
        return FooA(prop)
    elif prop == PROP_VAL_2:
        return FooB(prop)

By definition, you want the class Foo to return an instance that is not Foo, but a subclass of Foo.

I would caution against this, as it may be surprising to the users of your class.

You have a few options:

You can use a factory function, as @Barmar suggests but use a name such as foo_factory and keep the method Foo.method intact with some aggressive logging. This allows you to incrementally change all call sites from Foo to foo_factory and have the logging as peace of mind that no one is directly instantiating Foo anymore.

Another option is to modify Foo.__new__ to return an instance of a subclass:

I modified your code a bit to an MRE.

PROP_VAL_1 = 1
PROP_VAL_2 = 2


class Foo:
    def __new__(cls, prop):
        # if the class was instantiated directly
        if cls is Foo:
            if prop == PROP_VAL_1:
                return FooA(prop)
            elif prop == PROP_VAL_2:
                return FooB(prop)
            else:
                raise ValueError(f"Invalid value {prop}")
        # this is only reached if a subclass is instantiated
        return super().__new__(cls)
    
    def __init__(self, prop):
        self.prop = prop


class FooA(Foo):
    def method(self):
        return 'do_something'


class FooB(Foo):
    def method(self):
        return 'do_something_2'


foo = Foo(prop=2)
print(isinstance(foo, Foo)) # True
print(type(foo)) # <class '__main__.FooB'>
print(foo.method()) # do_something_2
Related