Here’s a proposal that covers both the Callable case and also the “signature” case (explicit specification of ParamSpec values).
Which keyword?
I agree that Fn (or fn) would be a better keyword, and it’s also true that Fn would work as a soft keyword in something like Fn (int) -> str because we can backtrack from the ->, but it wouldn’t work for the “signature” use case, where we don’t specify a return type (see below). So, I still think def is the only option.
The “signature” use case
I think the easiest way to demonstrate this use case is something like this:
from collections.abc import Callable
type CallableReturningInt[**P] = Callable[P, int]
This type alias can be used like this (works in pyright and mypy master):
def f(x: int) -> int:
return x
g: CallableReturningInt[[str]] = f # Error: "str" incompatible with "int"
h: CallableReturningInt[[int]] = f # OK
But we are severely limited in what kind of signatures we can specify for CallableReturningInt. We can only specify positional arguments: CallableReturningInt[[str, int, float]]. It would be nice to be able to specify more complex signatures.
Proposed new syntax
Part 1
def followed by parentheses (, ), now creates a new object called Signature:
sig = def (x: int, /, y: str, *, z: bool)
# Equivalent to
sig = Signature([
Parameter(name='x', kind=POSITIONAL_ONLY, annotation=int),
Parameter(name='y', kind=POSITIONAL_OR_KEYWORD, annotation=str),
Parameter(name='z', kind=KEYWORD_ONLY, annotation=bool),
])
These signatures can then be used in types as the value for a ParamSpec:
type IntToInt = CallableReturningInt[def (x: int)]
But with the benefit that static type checkers can understand this now, and can treat it as roughly equivalent to
type IntToInt = CallableReturningInt[[int]]
but more precise, because we were also able to give the name of the function parameter.
Part 2
The most common use of signatures will be in Callables, so they get a special syntax:
def followed by parentheses (, ), and then followed by ->, is syntactic sugar for a Callable with the given signature and return type:
type MyFunc = def (x: str) -> int
# Equivalent to
type MyFunc = Callable[def (x: str), int]
# Which in turn is roughly equivalent to
type MyFunc = Callable[[str], int]
Downsides
- the use of
def to define not functions but signature objects and types might be confusing to people
- the fact that there are two new syntax constructs involving
def might be even more confusing
Upside
This solves two problems at once: complex callables are currently awkward to specify (with Protocol), and ParamSpec values can only be specified in a very limited way.