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?
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With