Allow `P.args` as a type annotation?

It’s more than a little common to take (*args, **kwargs) and then save them or pass them elsewhere as (args, kwargs), but currently there’s no way (that I can find) to type such a thing.

Please consider allowing a type that’s parameterized on a ParamSpec P to use P.args and P.kwargs as annotations outside of the currently required *P.args, **P.kwargs constraint.

Examples:

A)
This class is currently allowed, and the type of Call.args here is P.args but there does not exist a valid explicit annotation for it:

class Call(Generic[P]):
    def __init__(self, *args: P.args, **kwargs: P.kwargs):
        super().__init__()
        self.args = args
        self.kwargs = kwargs

B)
This signature is currently impossible to annotate. It will need some major invasive surgery to become typed, but it’s not even clear what that change would be.

class _lru_cache_wrapper(Generic[P, T]):
    def make_key(self, args: P.args, kwds: P.kwargs): ...

C)
I’d like to propose allowing these (or some equivalent) definitions:

class Call(Generic[P], tuple[P.args, P.kwargs]):
    def __new__(cls, iterable: tuple[P.args, P.kwargs]):
        return super().__new__(cls, iterable)

    @classmethod
    def from_call(cls, *args: P.args, **kwargs: P.kwargs):
        return cls((args, kwargs))

Call: TypeAlias = tuple[P.args, P.kwargs]
3 Likes

I think I’d want to see further examples, right now your examples look extremely possible to annotate, for example

class Call(Generic[P]):
    def __init__(self, *args: P.args, **kwargs: P.kwargs):
        super().__init__()
        self.args: tuple[Any, ...] = args
        self.kwargs: dict[str, Any] = kwargs

(and in fact, if you rely on the type of self.args or self.kwargs being narrower than this, Paramspec use isn’t appropriate to begin with)

If this is for typing functools.lru_cache as appears to be indicated by example B, make_key doesn’t need the precise types, it operates on arbitrary args and kwargs, so this is okay here too.

Any is how we turn off typing, so that doesn’t seem typed to me.

1 Like

It’s unfortunate that people view Any as a magic off switch for typing and try to avoid it as a result, sometimes it’s the correct type, including when you accept anything and don’t rely on the specifics of the type, such as exactly the case with functools cache handling of make_key

(object isn’t correct here because of the variance of dict)

1 Like

Do you mean allowing something like this to pass?

I’d also be curious if it’s feasible and consistent to allow it.

I’d also be curious if it’s feasible and consistent to allow it.

No, that wouldn’t work. The problem is that callable signatures can include parameters that receive values from either positional or keyword arguments. The mix of positional vs keyword args can vary from caller to caller.

Consider the following:

def func(a: int, b: int): ...

# All three of these calls supply valid arg lists
func(1, 2) # 2 positional args, 0 keyword args
func(1, b=2) # 1 positional arg, 1 keyword arg
func(a=1, b=2) # 0 positional args, 2 keyword args

If a ParamSpec is specialized to the signature of func in this example, there’s no way to know statically whether there are zero, one or two positional args.

This is why P.args and P.kwargs always need to be used together. As a pair, they describe the entire input signature. Each of these by itself has no valid meaning. For that reason, tuple[P.args, P.kwargs] doesn’t make sense.

5 Likes