typing icon indicating copy to clipboard operation
typing copied to clipboard

Option to type default default arguments in Callable types - `WithDefault`

Open cdce8p opened this issue 3 years ago • 5 comments

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

cdce8p avatar Aug 03 '22 15:08 cdce8p

This makes me want to re-submit PEP 677…

gvanrossum avatar Aug 03 '22 15:08 gvanrossum

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.

cdce8p avatar Aug 03 '22 16:08 cdce8p

A special type also has the benefit that it could be used in other places, too. See #1231

cdce8p avatar Aug 03 '22 16:08 cdce8p

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.

hmc-cs-mdrissi avatar Aug 03 '22 23:08 hmc-cs-mdrissi

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.

cdce8p avatar Aug 04 '22 00:08 cdce8p