Option to type default default arguments in Callable types - `WithDefault`
At the moment it's only possible to type callables with default arguments by using a Callback protocol . This adds a lot of additional code for just a few default arguments. Moreover it requires that users have at least a basic understanding of Protocols which is one of the more advanced typing concepts. Lastly, a Callback protocol can't be used to type a generic ParamSpec argument.
Proposal
Add a new WithDefault special type which can be used to annotate arguments in callable types.
Examples
from typing import Callable, TypeAlias, WithDefault
def func(a: str, b: int = 0) -> None: ...
def other(a: str, b: int) -> None: ...
def g(f: Callable[[str, WithDefault[int]], None]) -> None:
f("Hello") # ok
f("World", 2) # ok
g(func) # ok
g(other) # error
For generic ParamSpec types
class Job(Generic[_P]):
def __init__(self, target: Callable[_P, None]) -> None:
self.target = target
def g(job: Job[str, WithDefault[int]]) -> None:
job.target("Hello") # ok
job.target("Hello", 2) # ok
This makes me want to re-submit PEP 677…
This makes me want to re-submit PEP 677…
I agree, a good callable syntax would have been nice but I don't see that happening anytime soon.
Adding a special type might be verbose, but it's a least something that can be done.
--
This proposal could even be extended to add special types for
PosOnly and KwOnly. Certainly not ideal to have such a verbose solution, however it could then be a good argument for a new shorthand notation given some time.
A special type also has the benefit that it could be used in other places, too. See #1231
Lastly, a Callback protocol can't be used to type a generic ParamSpec argument.
I'm confused by meaning of this statement. I use callback protocols like
class TriggerCondition(Protocol[_P]):
__name__: str
def __call__(
self,
job_id: str,
*args: _P.args,
**kwargs: _P.kwargs,
) -> object:
...
Can you clarify the statement?
I'm a bit skeptical that protocol is much more complex then knowing when to use WithDefault. If you need to deal with callbacks where default/positional status of argument matters I'd be surprised if you weren't already familiar with protocols. The one brevity it gives you is skipping names and staying positional only, but I'm meh on having more variety for typing callables (unlesss pep 677 or variation becomes feasible). I like callback protocols as they just follow normal def syntax.
Lastly, a Callback protocol can't be used to type a generic ParamSpec argument.
I'm confused by meaning of this statement. I use callback protocols like [...]
Can you clarify the statement?
It's not about using ParamSpec within a Callback protocol. Rather how you type a generic ParamSpec to indicate it has default arguments.
from typing import Callable, ParamSpec, Generic, Protocol
_P = ParamSpec("_P")
class Func(Protocol):
def __call__(self, a: str, b: int = 0) -> None: ...
class Job(Generic[_P]):
def __init__(self, target: Callable[_P, None]) -> None:
self.target = target
def f(action: Func) -> Job: # How should Job be typed here?
return Job(action)
Func is a Callback protocol with default values. What should the concrete return type for f be? Neither Job[str, int] nor Job[str] is correct. Both pyright and mypy have internal representations for it, but to my knowledge, this isn't expressible with the current type system.
reveal_type(Job(action)
# pyright
# Job[(a: str, b: int = ...)]
# mypy
# Job[[a: builtins.str, b: builtins.int =]]
This is only a simplified example. I'm aware that it's probably better to use a normal TypeVar for the whole Callable type instead of the ParamSpec. That isn't possible in the specific case though.