I think it's important to be aware of the subtle differences between *function
type annotations* and *callable types*. The type annotations on a function
definition (or stub) define *all* the possible ways the function can be
called. Callable types on the other hand define *one* particular way a
function can be called. When you're writing type annotations for a callback
argument, you base it on how the callback will be *called*, not how any
particular function is *defined*.
These differences also mean we have slightly different requirements from
the syntax. A function definition can have three types of arguments:
positional-or-keyword arguments (the default), positional-only arguments
and keyword-only arguments. For a callable type on the other hand there are
two types (which correspond to how the function will actually be called):
positional arguments and keyword arguments.
Due to these differences I don't think the "stub syntax" makes sense. In
order to make it unambiguous we would have to disallow
positional-or-keyword arguments, so you would always need to use / or * or
both. And even then it wouldn't be quite intuitive to newcomers that a
callable type like (a: int, /) -> bool would also match a function defined
as def foo(bar: int, baz: int = 0) -> bool. It's better to use new
intuitive syntax than to use existing syntax in unintuitive ways IMO.
The hybrid syntax makes more sense, but I would not allow * and / in this
syntax since they have no useful meaning in this context. There is also no
point being able to specify that a named argument should be optional - it
will either be given or not when calling the function, so it's irrelevant
whether the implementation provides a default value.
Variadic arguments are still needed, but giving them names would have no
meaning. Here are a few options I can think of for how they could be
- (*: int, **: Any)
- (*int, **Any)
- (*int, *: Any)
- (*int, **: Any)
I think the second option would be preferable, because it matches the
syntax for unpacking. On the other hand, the last two keep the convention
of using ":" for keyword arguments. To be clear about the meaning of this
syntax: Any matching function *has to have* corresponding *args and
**kwargs in its definition - variadic arguments in the callable type would
never match non-variadic required arguments in the function definition.
To give an example of how the hybrid syntax could be used in practice,
consider the following function taking a callback argument:
def foo(x: int, callback: ??) -> bool:
y = "bar"
return callback(x, y, asdf=6.5)
From this, writing the type annotation for the callback is straightforward:
callback: (int, str, asdf: float) -> bool
And one with variadic arguments:
def foo(x: Sequence[int], callback: (*int, **Any) -> bool, **kwargs: Any)
return callback(*x, **kwargs)
I'm also in favor of the proposed syntax of (...) -> T to cover the case
where it is not statically known how the function might be called, e.g. for
We should also consider how the new syntax will interact with PEP 612
(ParamSpec). Should the following modified example from the PEP be allowed?
P = ParamSpec("P")
R = TypeVar("R")
def add_logging(f: P -> R) -> (P -> Awaitable[R]):
Or perhaps even something like this?
def with_request(f: (Request, *P.args, **P.kwargs) -> R) -> (P -> R):