Skip to content

Option to type default default arguments in Callable types - WithDefault #1232

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
cdce8p opened this issue Aug 3, 2022 · 5 comments
Open
Labels
topic: feature Discussions about new features for Python's type annotations

Comments

@cdce8p
Copy link
Contributor

cdce8p commented Aug 3, 2022

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 cdce8p added the topic: feature Discussions about new features for Python's type annotations label Aug 3, 2022
@gvanrossum
Copy link
Member

This makes me want to re-submit PEP 677…

@cdce8p
Copy link
Contributor Author

cdce8p commented Aug 3, 2022

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
Copy link
Contributor Author

cdce8p commented Aug 3, 2022

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

@hmc-cs-mdrissi
Copy link

hmc-cs-mdrissi commented Aug 3, 2022

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.

@cdce8p
Copy link
Contributor Author

cdce8p commented Aug 4, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: feature Discussions about new features for Python's type annotations
Projects
None yet
Development

No branches or pull requests

3 participants