Class Inheritance for Parameters in Python

Posted in December 2020 to programming

It sometimes happens that I'm doing something at work, hit an issue, and don't really have the functional reserve (in terms of time, motivation, or ... HP? MP?) to properly deal with it. My usual solution is to spend a few minutes doofing about with it to see if a quick zero-effort fix will get the job done, and then come back to it later. Usually, these problems come about because of some fundamental misunderstanding of how something works, or is supposed to work, so reviewing these things when I have the time often results in an "aha!" moment, and sweet sweet resolution!

The topic this time fits squarely into the above category. I've never done programming in a language that strictly enforced OOP, and I feel as if Python spoils me by giving me a way out: "Hey, don't feel like doing any OOP today? It's OK, you don't have to!" Well, this time there was no way out, so here I'll outline my original problem, how I tried to fudge a solution, and then show how it should be done.

I had a module where a class and several subclasses of that class were defined. In my first version, none of the class definitions took any arguments, but I wanted to add parameters to the parent class and have them be inherited by the subclasses. For parent class A, which has keyword parameters x and y, I wanted to be able to create an instance of subclass B like this:

vars = {"x": 3, "y": 5}

obj = B(**vars)

but it didn't work. Here's the full code of my first attempt:

# DOES NOT WORK
class A:
    def __init__(self, x=1, y=2):
        self.x = x
        self.y = y

    def sum(self):
        return self.x + self.y


# DOES NOT WORK
class B(A):
    def __init__(self):
        super().__init__()


obj = B(x=3, y=5)

# TypeError: __init__() got an unexpected keyword argument 'x'

No good. I could see that the parameters needed to be handled by B, and knew that passing self around was involved somewhere, so I tried this:

# DOES NOT WORK
class A:
    def __init__(self, x=1, y=2):
        self.x = x
        self.y = y

    def sum(self):
        return self.x + self.y


# DOES NOT WORK
class B(A):
    def __init__(self, **kwargs):
        super().__init__(self, **kwargs)


obj = B(x=3, y=5)

# TypeError: __init__() got multiple values for argument 'x'

After removing that self part, I found something that worked:

# WORKS!
class A:
    def __init__(self, x=1, y=2):
        self.x = x
        self.y = y

    def sum(self):
        return self.x + self.y


class B(A):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)


obj = B(x=3, y=5)
obj.sum()

# returns 8

So the problem was throwing that self in there. As documented in the Python docs, the code in the previous example does the same thing as:

# ALSO WORKS!
class A:
    def __init__(self, x=1, y=2):
        self.x = x
        self.y = y

    def sum(self):
        return self.x + self.y


class B(A):
    def __init__(self, **kwargs):
        super(B, self).__init__(**kwargs)


obj = B(x=3, y=5)
obj.sum()

# returns 8

Someday I'll do a deep dive into super() and MRO, and link to it from here, but for today I'm happy to have solved my problem. On a final note, another common pattern is where there are different parameters for the parent and child class. Here is an example that doesn't work:

# DOES NOT WORK
class A:
    def __init__(self, x=1, y=2):
        self.x = x
        self.y = y

    def sum(self):
        return self.x + self.y


# DOES NOT WORK
class B(A):
    def __init__(self, **kwargs, z=3):
        self.z = z
        super(B, self).__init__(**kwargs)


#     def __init__(self, **kwargs, z=3):
#                                  ^
# SyntaxError: invalid syntax

Instead, you can remove the entry from the dict, as shown below. It's also possible to set the default value, for where the key isn't included: here it's set to 3.

# WORKS
class A:
    def __init__(self, x=1, y=2):
        self.x = x
        self.y = y

    def sum(self):
        return self.x + self.y


class B(A):
    def __init__(self, **kwargs):
        self.z = kwargs.pop("z", 3)
        super(B, self).__init__(**kwargs)

    def get_z(self):
        return self.z


obj = B(x=4, y=3)
obj.sum()

# returns 7

obj.get_z()

# returns 3