(This is a continuation of a discussion in PEP Idea: Extend spec of Callable to accept unpacked TypeDicts to specify keyword-only parameters where the thread-starter wished to focus back on the original topic.)
PEP 677 defined a new callable type syntax:
def flat_map(
func: (int) -> list[int],
values: list[int],
) -> list[int]:
out = []
for element in values:
out.extend(f(element))
return out
But the PEP was rejected. Guido summarized the rejection note here: PEP Idea: Extend spec of Callable to accept unpacked TypeDicts to specify keyword-only parameters - #17 by guido
One way to read it is that in order to get something like PEP 677 accepted, the following needs to be true:
- The syntax should be parsable without the new PEG parser.
- The syntax should allow expressing things that were previously not possible.
He then suggested a syntax which re-uses lambda expressions:
def flat_map(
func: lambda(x: int) -> list[int]: ...,
values: list[int],
) -> list[int]:
out = []
for element in values:
out.extend(f(element))
return out
The idea is essentially to allow annotating lambda with type annotations, and then to use a lambda which only returns Ellipsis to be used as a callable type. This is easier to parse due to the lambda keyword and it allows expressing things that were previously not possible, like keyword-only and optional arguments:
def f(func: lambda(y: float, *, z: bool = ...) -> bool: ...) -> None:
pass
It’s probably uncontroversial to say that most people will not like the need for the Ellipsis return. The Ellipsis return will also look quite confusing when a function returns a callable. But if it was possible to remove that part (and also make function parameter names optional), it seems to me like this is a good syntax:
def flat_map(
func: lambda(int) -> list[int],
l: list[int],
) -> list[int]: ...
type StrTransform = lambda(str) -> str
def f() -> lambda(int, str) -> bool:
...
Why lambda instead of def?
At first I was skeptical, but I now think there are good reasons to choose lambda over def for this:
defreally sounds like you’re defining something, which you aren’t really heredeflooks a bit confusing in the annotation of a function parameter because there’s already adefthere from the function itself- currently,
defalways expects a function name;lambdadoesn’t, solambdafits the job a bit better
What are potential downsides?
The major downside is probably that at first, function signatures with a lambda callable type will be harder to parse for the human eye. Especially when there is also a lambda expression default argument:
def flat_map(
l: list[int],
func: lambda(int) -> list[int] = lambda x: [x],
) -> list[int]: ...
But I think the parentheses make these sufficiently visually distinct, such that one will get used to it pretty quickly.