Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should I repeat parent class __init__ arguments in the child class's __init__, or using **kwargs instead

Tags:

python

Imagine a base class that you'd like to inherit from:

class Shape:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

There seem to be two common patterns of handling a parent's kwargs in a child class's __init__ method.

You can restate the parent's interface completely:

class Circle(Shape):
    def __init__(self, x: float, y: float, radius: float):
        super().__init__(x=x, y=y)
        self.radius = radius

Or you can specify only the part of the interface which is specific to the child, and hand the remaining kwargs to the parent's __init__:

class Circle(Shape):
    def __init__(self, radius: float, **kwargs):
        super().__init__(**kwargs)
        self.radius = radius

Both of these seem to have pretty big drawbacks, so I'd be interested to hear what is considered standard or best practice.

The "restate the interface" method is appealing in toy examples like you commonly find in discussions of Python inheritance, but what if we're subclassing something with a really complicated interface, like pandas.DataFrame or logging.Logger?

Also, if the parent interface changes, I have to remember to change all of my child class's interfaces to match, type hints and all. Not very DRY.

In these cases, you're almost certain to go for the **kwargs option.

But the **kwargs option leaves the user unsure about which arguments are actually required.

In the toy example above, a user might naively write:

circle = Circle()  # Argument missing for parameter "radius"

Their IDE (or mypy or Pyright) is being helpful and saying that the radius parameter is required.

circle = Circle(radius=5)

The IDE (or type checker) is now happy, but the code won't actually run:

Traceback (most recent call last):
  File "foo.py", line 13, in <module>
    circle = Circle(radius=5)
  File "foo.py", line 9, in __init__
    super().__init__(**kwargs)
TypeError: Shape.__init__() missing 2 required positional arguments: 'x' and 'y'

So I'm stuck with a choice between writing out the parent interface multiple times, and not being warned by my IDE when I'm using a child class incorrectly.

What to do?

Research

This mypy issue is loosely related to this.

This reddit thread has a good rehearsal of the relevant arguments for/against each approach I outline.

This SO question is maybe a duplicate of this one. Does the fact I'm talking about __init__ make any difference though?

I've found a real duplicate, although the answer is a bit esoteric and doesn't seem like it would qualify as best, or normal, practice.

like image 781
LondonRob Avatar asked Sep 07 '25 02:09

LondonRob


1 Answers

If the parent class has required (positional) arguments (as your Shape class does), then I'd argue that you must include those arguments in the __init__ of the child (Circle) for the sake of being able to pass around "shape-like" instances and be sure that a Circle will behave like any other shape. So this would be your Circle class:

class Shape:
    def __init__(x: float, y: float):
        self.x = x
        self.y = y


class Circle(Shape):
    def __init__(x: float, y: float, radius: float):
        super().__init__(x=x, y=y)
        self.radius = radius


# The expectation is that this should work with all instances of `Shape`
def move_shape(shape: Shape, x: float, y: float):
    shape.x = x
    shape.y = y

However if the parent class is using optional kwargs, that's where stuff gets tricky. You shouldn't have to define colour: str on your Circle class just because colour is an optional argument for Shape. It's up to the developer using your Circle class to know the interface of all shapes and if need be, interrogate the code and note that Circle can accept colour=green as it passes **kwargs to its parent constructor:

class Shape:
    def __init__(x: float, y: float, colour: str = "black"):
        self.x = x
        self.y = y
        self.colour = colour 


class Circle(Shape):
    def __init__(x: float, y: float, radius: float, **kwargs):
        super().__init__(x=x, y=y, **kwargs)
        self.radius = radius


def move_shape(shape: Shape, x: float, y: float):
    shape.x = x
    shape.y = y


def colour_shape(shape: Shape, colour: str):
    shape.colour = colour

Generally my attitude is that a docstring exists to explain why something is written the way it is, not what it's doing. That should be clear from the code. So, if your Circle requires an x and y parameter for use in the parent class, then it should say as much in the signature. If the parent class has optional requirements, then **kwargs is sufficient in the child class and it's incumbent upon the developer to interrogate Circle and Shape to see what the options are.

like image 69
Daniel Quinn Avatar answered Sep 10 '25 14:09

Daniel Quinn