Repository navigation
Proposal: Generalize Callable to be able to specify argument names and kinds #264
Description
Right now you can specify callables with two patterns of arguments (shown here by example):
Callable[..., int]takes in any arguments, any number.Callable[[int, str, bool], int]takes in a predetermined number of required positional arguments, none of which have names specified.
These don't cleanly match the actual types of callable objects in Python. Argument names, whether arguments are optional, and whether arguments are *args or **kwargs do affect the type of a callable. We should be able to spell these things in the type language. Doing so would enable us to correctly write the types of callback functions, for example.
Callable should take two arguments: an argument list and a return type. The return type is exactly as currently described in PEP484. The argument list is either:
..., indicating the function can take any arguments at all.- Square brackets around a comma separated series of argument specifiers (argspec for short), indicating particulars about the functions arguments
An argument specifier is one of:
- A bare type
TYP. This has the same meaning asArg(TYP) Arg(type, name=None), indicating a positional argument. If the name is specified, the argument must have that name.OptionalArg(type, name=None), indicating an optional positional argument. If the name is specified, the argument must have that name. (alternate name possibilityOptArg(type, name=None)StarArg(type), indicating a "star argument" like*argsKwArg(type), indicating a "double star argument" like**kwargs. (an alternate name here would beStar2Arg(type).
The round parens are an indication that these are not actual types but rather this new argument specifier thing.
Like the rules for python function arguments, all positional argspecs must come before all optional argspecs must come before zero or one star argspecs must come before zero or one kw argspecs.
This should be able to spell all function types you can make in python by defining single functions or methods, with the exception of functions that need SelfType to be properly specified, which is an orthogonal concern.
Some statements I think are true:
Callable[[Arg(T1, name='foo'), Arg(T2, name='bar')], R]is a subtype ofCallable[[T1, T2], R]Callable[[T1, OptionalArg(T2)], R]is a subtype ofCallable[[T1], R]Callable[[StarArg(T1)], R]is a subtype ofCallable[[], R]and is also a subtype ofCallable[[T1], R]and is also a subtype ofCallable[[T1, T1], R]and so on.Callable[[T1, StarArg(T1)], R]is a subtype ofCallable[[T1], R]and is also a subtype ofCallable[[T1, T1], R]and so on, but is not a subtype ofCallable[[], R]- If we want to be able to spell overloaded function types we'll need a specific way of combining callable types in the specific way overloading works; this proposal doesn't address that.
Activity
JukkaL commentedon Aug 19, 2016
Another thing to consider is keyword-only arguments in Python 3. Keyword-only arguments can be optional or non-optional. Maybe use
Arg(t, name='foo', keyword_only=True), and similarly forOptionalArg?sixolet commentedon Aug 19, 2016
@JukkaL Yep seems good.
ilevkivskyi commentedon Aug 19, 2016
@sixolet Thankyou for your proposal!
I proposed a similar idea in #239 based on
inspect.Signatureand Guido rejected it in favor of function stubs. His argument (and I agree with him) is readability. Compare:and
I think the first one is more readable and allows to quickly grasp the function signature. Also it is not clear from your proposal how to type annotate a decorator that preserves the types of all arguments in
*args(propbably you also need to discuss how this interacts with variadic generics etc).I would like to reiterate, function stubs together with
OtherArgsdiscussed in #239 cover vast majority of use cases, so that I think practicality beats purity here.@JukkaL what do you think about the magic (variadic) type variable
OtherArgs? I copy here use cases in decorators from previous discussions:sixolet commentedon Aug 19, 2016
Regarding
OtherArgs, I think it's separable and so we should consider it separately; being able to spell argument names and kinds is useful in the absence of such a thing. I'll write it up separately once we know what the fate of variadic type variables is likely to be (since the exact definition/meaning ofOtherArgsdepends on whether we can explain it with variadic typevars).sixolet commentedon Aug 19, 2016
It's true that this proposal is similar to your proposal based on
inspect.Signature, but it differs in a few important ways:JukkaL commentedon Aug 19, 2016
@ilevkivskyi I agree with @sixolet that
OtherArgsshould be discussed separately from this. This proposal (or the alternative proposed syntax) would be useful as such. Callbacks with other than positional arguments have been a pretty common mypy feature request.ilevkivskyi commentedon Aug 19, 2016
OK, if you want to proceed with this, I would propose to try to make it more concise. Here are some tweaks, apart from already mentioned
OpArgs:namea first argument,Arg(T)it is just equivalent toT, so that better make the type optional:kw_only=True,kw_onlyin obvious places, e.g., afterStarArg,StarArgandKwArgshould also meanAny(like actually everywhere in PEP 484, missing type meansAny).With all these tweaks it will look like this:
has type
Second example:
has type
It looks more concise, and still readable. What do you think?
sixolet commentedon Aug 19, 2016
@ilevkivskyi I like most of that.
The only bit I think I disagree with is
And that's because I think in this particular case consistency is more important than brevity.
Huh. Can we omit the parens when they're not being used for anything? Why not
sixolet commentedon Aug 19, 2016
(Answering my own question, the round parens remind us it's an argspec not a type. Right.)
ilevkivskyi commentedon Aug 19, 2016
@sixolet OK
Now that you have +1 from Jukka, you should get an approval from Guido and then add this to the PEP and typing.py.
By the way I was thinking a bit about how this will interoperate with variadic generics, and I think the solution is simple:
StarArgandKwArgshould be allowed to accept them as an argument and that's it. Then you could easily type a decorator with something likeMoreover
OtherArgscould be easily defined as[StarArg(As), KwArg(Ks)], so that one can write (unpacking just unpacks two items here):But now it is probably not needed in the specification. So that now I give your proposal a solid +1.
elazarg commentedon Oct 21, 2016
I don't understand what's wrong with something like
Callable[lambda a: int, *, b: str =...:...]It will be much easier for type checkers to learn, and the meaning is obvious. Why invent new ways of saying the same thing?
JukkaL commentedon Oct 21, 2016
It's not syntactically valid. Lambdas can't have type annotations.
sixolet commentedon Oct 25, 2016
@Lazarg Also, we should be careful to avoid confusing people regarding "how do you write a function" vs "how do you write the type of a function"
elazarg commentedon Oct 25, 2016
The type (the interface) of the function is the thing that appears right after its name - the signature.
elazarg commentedon Oct 25, 2016
Oh. Consequences of using : instead of => ?
That's very unfortunate.
21 remaining items
devxpy commentedon Nov 7, 2018
@ilevkivskyi This does not seem to work with
*argsand**kwargs.Am I doing something wrong?
ilevkivskyi commentedon Nov 7, 2018
@Seanny123 Now that we have callback protocols, the PEP will be less valuable (if needed at all). The only missing thing is variadic generics, but this is a separate question, and the problem with them is not who will write the PEP, but who will implement it.
@devxpy Well, mypy correctly says that type of
get()is not a subtype ofApplesWrapper, there are many possible calls toApplesWrapperthat will fail onget().I think what you want is
OtherArgsproposed in #239 and also discussed a bit above. This is more about variadic generics which, as I explained above, is a separate issue.Better callable: `Callable[[Arg('x', int), VarArg(str)], int]` now a …
JukkaL commentedon May 30, 2019
Should we close this issue now that callback protocols are described in PEP 544 (which was accepted)?
gvanrossum commentedon May 30, 2019
I seem to be in the minority, but I find callback protocols too verbose, and not very intuitive.
I'd rather have a decorator you can put on a dummy
def. E.g. instead ofI'd prefer
My reasons are hard to explain -- I find the need to use
class,Protocol,__call__andselffor the current solution rather noisy.JukkaL commentedon May 31, 2019
I agree that the current syntax is overly verbose. On the other hand complex callables don't seem to be required more than occasionally by most programmers. Maybe somebody will come up with a nicer and more concise syntax, though.
antonagestam commentedon May 31, 2019
To find similarity to another thing within the language,
async def, it might be interesting to explore a "def" keyword like below. Since the spec can't have a body, the colon and ellipsis/pass should be omitted in my opinion.Or, if we'd like to be even less verbose with an even shorter syntax, making this it's entirely "own" thing.
Michael0x2a commentedon May 31, 2019
Maybe we could add in some decorator that when present will promote some function into a callable protocol? Basically, make it so that type checkers treat Guido's second example as a shorthand for the first.
The decorator could just be the identity function at runtime. We could also make it literally construct the protocol class if the distinction between a function vs a callable object is important to preserve.
At least for mypy, I feel we could pretty easily implement this via the plugin system.
jdelic commentedon May 31, 2019
@antonagestam
I haven't fully thought this through, but right now I like your idea... as it also specifies return types and not just args I would go for
instead of
argspec.And then only declare the implementation's intention of fulfilling the contract using a decorator:
As the language already introduced class variable annotations and return type annotations I'd find a syntax extension more natural for "first class support" of function types than a decorator syntax that leads to typing being implemented counter-intuitively through a mixture of new language keywords and special code.
antonagestam commentedon May 31, 2019
@jdelic As for naming of this hypothetical keyword, would
signaturebe even better? I agree that argspec is not an accurate name.(I hope this is not the wrong forum for hypothetical discussions like this)
gvanrossum commentedon May 31, 2019
Note that such syntactical changes are hard to get accepted -- adding a reserved word to Python requires a PEP, a
from __future__ import ...statement, and several releases of deprecation warnings. (That's what it cost to introduceasync defandawait.)In contrast, a decorator is trivial to add -- you can just import it.
ilevkivskyi commentedon Jun 2, 2019
I am fine with the current way. Some reasons:
defwith the proposed syntax this would require supporting something like:
and add some tricky runtime machinery to allow
Other[int]. I would propose to just close this and move on. I think there are more important things to do than minor syntactic improvements for relatively rare use cases.ambv commentedon Nov 11, 2019
I always sided with the line of thinking described by Guido here. I didn't even think we'd need a special decorator for it. I always thought of function objects as "types with callable signatures". I've seen many cases where being able to pass an example function as a type would make things clearer instead of using
Callable[].In practice I don't mind specifying a Protocol with
__call__()much but the requirement to addselfas the first argument there adds confusion on top of the overall verbosity of this.gvanrossum commentedon Nov 11, 2019
It's a tough call. In the end I think we should just try to live with the Protocol solution for a while before we decide that it's too onerous. So I'm closing this.