Dynamically building ParamSpecs from callables

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?

2 Likes

PEP 692: Using TypedDict for more precise **kwargs typing The best use case for this pep I think is exactly these kinds of situations where you want to pass kwargs to another function while preserving type checker’s ability to understand the code. Using your example it could be written like,

class CheeseshopArgs(TypedDict):
  red_leicester: bool
  tilsit: bool
  caerphilly: bool
  stilton: bool

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:
  ...

class FineCheeseshop(Cheeseshop):
  def __init__(self, limburger: bool = False, **kwargs: **CheeseshopArgs) -> None:

or if you’d like to avoid some duplication you could do,

class Cheeseshop:
    def __init__(
        self,
        **kwargs: **CheeseshopArgs
    ) -> None:
  ...

class FineCheeseshop(Cheeseshop):
  def __init__(self, limburger: bool = False, **kwargs: **CheeseshopArgs) -> None:
1 Like

I wasn’t aware of PEP-692. That would be awesome for classes I control, which is the likely to be the majority case here!

However, it won’t work for

  • positional args
  • classes from 3rd party libraries

I’ll ask on the TypedDict thread if ParamSpec has been considered (since I think it has the right interface).

Thanks for the pointer.

1 Like