Suppose I have a class with an initializer but then I want to add a subclass which adds a keyword argument. I’m going to show it written a particular way and then explain why it’s written that way.
class Cheeseshop:
def __init__(
self,
# much as I want to, I won't list all of the cheeses...
red_leicester: bool = False,
tilsit: bool = False,
caerphilly: bool = False,
stilton: bool = False,
) -> None:
self.red_leicester = red_leicester
self.tilsit = tilsit
self.caerphilly = caerphilly
self.stilton = stilton
class FineCheeseshop(Cheeseshop):
def __init__(
self,
red_leicester: bool = False,
tilsit: bool = False,
caerphilly: bool = False,
stilton: bool = False,
limburger: bool = False
) -> None:
super().__init__(
red_leicester=red_leicester,
tilsit=tilsit,
caerphilly=caerphilly,
stilton=stilton
)
self.limburger = limburger
Many python programmers would look at this structure and tend to expect FineCheeseshop
to be defined as follows:
class FineCheeseshop(Cheeseshop):
def __init__(self, limburger: bool = False, **kwargs) -> None:
super().__init__(**kwargs)
self.limburger = limburger
However, doing so discards typing information. Writing out the class explicitly allows the author to retain all of the type annotations from the base class.
It allows type checkers to catch FineCheeseshop(camembert=True)
as a type error (nevermind that it’s a bit runny).
The trouble is that we can’t annotate **kwargs
to make it match the keyword args of the base class.
However, if we could build a ParamSpec to match the signature of an arbitrary callable, then we could construct one from Cheeseshop.__init__
and then pull the kwargs
off of there:
# proposed construction, exact usage and name TBD
P = BoundParamSpec(Cheeseshop.__init__)
class FineCheeseshop(Cheeseshop):
def __init__(
self,
*args: P.args,
limburger: bool = False,
**kwargs: P.kwargs
) -> None:
super().__init__(**kwargs)
self.limburger = limburger
As a further note on the utility of this proposal, the base class may come from a 3rd party library. That can make **kwargs
more appropriate at runtime (handles any new arguments gracefully) but still less appropriate at type-checking time.
Are there better solutions today than what I’m proposing? Big holes in this argument or line of thinking? Or is this a good idea?