I think wrapping a callable function while related is fundamentally different and not what is needed for this use case. We already have a way to wrap callables for decorators using Paramspec. A simple example is you can do,
def log_decorator(func: Callable[P, R]) -> Callable[P, R]:
def _func(*args: P.args, **kwargs: P.kwargs) -> R:
logger.info('some debug stuff")
return _func(*args, **kwargs)
The Line2D example is fundamentally different. We are not wrapping all of arguments of Line2D, but only **kwargs portion. We do not wrap x, y arguments it has. Even if we had Signature[Line2D] type tool it would not capture right thing.
If we did follow path of Signature[Line2D] we’d have two options,
- Exclude[Signature[Line2D], x, y]. Defining behavior of removing arguments sounds high complexity and we currently lack simpler ability to add keyword arguments to a captured signature with paramspec. Even with this we still have a question of what type does this refer as kwargs from original Line2D lacks information.
- Signature[Line2D].kwargs. This is a lot less useful then it sounds though. If matplotlib Line2D kwargs is untyped, then Signature[Line2D].kwargs would end up being just Any as a type checker could do no real checks on it. For it to be useful requires a way to annotate kwargs with type of values in it, which is exactly what this PEP does.
If we had this pep then 2 makes some sense as a way to refer to kwargs type without knowing name of annotation. But without this pep there’s no way to provide information about color/linestyle and have useful validation occur. You can’t wrap signature of Line2D in a useful manner without first having a way to type hint signature fully of
Line2D.__init__. Paramspec pep is devoted to wrapping callables, but wrapping an untyped argument similarly has little meaning.
Also this pep fits in well with situation of *args. When type system was started in pep 484 you just did,
def foo(*args: int):
to mean all arguments are int. With PEP 646 (variadic generics pep) it is possible to do heterogenous arguments in similar fashion as this one,
def foo(*args: *tuple[int, str]):
This example with *args is already accepted for 3.11 and can be used right now in some type checkers to mean one int and one string argument. I think there’s less value of doing explicit expansion with *args, but we have better sense that this pattern fits and we have 1 type checker already supporting this PEP provisionally, with a draft implementation for mypy too.
Even if we were to go back in type, it’s unlikely dict[str, int] way of doing kwargs would make much sense. Back then typeddicts didn’t exist. And there are a bunch of functions where them being same type is fine (often object is type doesn’t matter much).
I’ll admit downside of confusion. I’ve seen a number of people try to write,
def foo(**kwargs: dict[str, object]):
or similar and not realize that’s probably not what they want (maybe linter could warn for this). But I consider that confusion to be orthogonal to this PEP given large backward compatibility break if we changed that.