Optional Syntax for Typed Lambda Expressions

Title: [Pre-PEP] Optional Syntax for Typed Lambda Expressions

Hi everyone,

I would like to propose an optional syntax extension for Python’s lambda expressions to support parameter type annotations and generic type parameters.

Below is the draft abstract and specification. I am looking for feedback on the syntax choices and the rationale regarding modern typing workflows.


Abstract

This proposal introduces an optional syntax extension for Python’s lambda expressions to support parameter type annotations and generic type parameters.

To maintain parsing clarity and visual consistency with existing function definitions, typed parameters are required to be enclosed in parentheses, mirroring the syntax of def statements. Generic type parameters are placed in square brackets immediately after the lambda keyword, following the pattern established by PEP 695.

The legacy untyped lambda syntax remains fully unchanged and supported.

Rationale

Python’s type hinting system (PEP 484, PEP 695, etc.) has become a cornerstone of modern Python development. However, lambda expressions remain the only callable construct that cannot participate fully in this system.

Historically, extending lambda with annotations was viewed with caution to discourage overly complex anonymous functions. However, the landscape has evolved. Many functions that are logically simple (identity, wrappers, minor transformations) now require explicit annotations to satisfy strict static analysis, especially involving nested containers or generics.

Currently, developers must choose between:

  1. Retaining the concise lambda (losing type safety).
  2. Introducing a named def (adding namespace pollution and separating definition from usage).

This proposal addresses this gap conservatively. By reusing the exact parameter syntax of def (parentheses) and PEP 695 (square brackets), it promotes consistency without altering the role of lambda as a lightweight tool.

Key benefits:

  • Consistency: Lambdas can express the same type contracts as named functions.
  • Tooling: Static analyzers gain reliable info for complex functional chains (e.g., Pandas, RxPY).
  • Locality: Truly one-off callbacks remain co-located with their usage.

Specification

The lambda expression gains two optional components when typing features are used:

  1. Generic type parameters in square brackets (per PEP 695).
  2. A parenthesised parameter list supporting full annotations and return arrow.

Formal Syntax

lambda [type_params] (parameters) -> return_type: expression
lambda [type_params] (parameters)               : expression

The [type_params] part is optional and follows PEP 695 exactly.

Examples

# Generic identity
lambda [T] (x: T) -> T: x

# Simple addition with annotations
lambda (x: int, y: int) -> int: x + y

# Single parameter without return annotation
lambda (x: str): x.upper()

# With default value
lambda (scale: float = 1.0) -> float: value * scale

# Generic with bound
lambda [T: int] (x: T, y: T) -> T: x + y

Legacy Form

The legacy untyped form remains unchanged:

lambda x, y: x + y

The parser distinguishes the forms by checking for [ or ( immediately after lambda.

Backwards Compatibility

This change is fully backwards compatible. All existing lambda expressions continue to work unchanged.

Note: The historical Python 2 form lambda (x, y): ... (tuple parameter unpacking) was removed in Python 3, so the parentheses syntax does not conflict with the new typed form.

Reference Implementation Notes

A draft modification to the PEG grammar (Grammar/python.gram) would add alternative branches to the lambdef rule:

lambdef[expr_ty]:
    | 'lambda' t=[type_params] '(' params=parameters ')' '->' r=expression ':' b=expression
        { _PyAST_TypedLambda(t, params, r, b, EXTRA) }
    | 'lambda' t=[type_params] '(' params=parameters ')' ':' b=expression
        { _PyAST_TypedLambda(t, params, NULL, b, EXTRA) }
    | 'lambda' a=[lambda_params] ':' b=expression
        { _PyAST_Lambda(a, b, EXTRA) }

A new TypedLambda AST node (or an extension of the existing Lambda node) is required to store type_params and returns fields.

Discussion

I welcome any feedback or suggestions on this proposal. I am particularly interested in:

  1. Whether the syntax feels consistent with the rest of the language.
  2. If there are any edge cases in the grammar that I might have overlooked.
  3. Whether the requirement for parentheses `lambda (x: int): …` is seen as an acceptable trade-off for parsing clarity.

Thank you for your time and looking forward to the discussion!

6 Likes

I would wait to introduce type parameters until pep 695 is accepted.

1 Like

Good news! PEP 695 was accepted three years ago and implemented in Python 3.12:

8 Likes

I like the suggestion, it’s simple and readable.

Just wanting to note that another thread recently proposed an alternate use-case for the currently illegal lambda(...) syntax. There hasn’t been any discussion there in a few months, but there was a decent amount of support garnered for the proposal. I’ll leave it up to others as to which proposal is a better use of this syntax.

That said, if AST-based type annotations get anywhere, both use-cases could be implemented independently.

2 Likes

Yeah, here is another proposal: Typed Anonymous Functions lamdef

Thanks for mentioning my proposal and having this idea here.

In my thread, @tstesen suggested following up single line typed lambda idea which I personally think is a more conservative but feasible first movement after PEP 3113 (ref).

I think what could be carefully designed is the position of the open parentheses.

lambda(a: int) vs lambda (a: int)

The first one to me seems more suitable for the long term consideration if my idea lamdef is explored further.

1 Like

Thank you for sharing this thoughtful insight, especially the subtle differences in the placement of parentheses. Your forward-thinking consideration of the lambda(a: int) form is indeed insightful, as it preserves clearer possibilities for future syntax extensions. Looking forward to seeing more discussion on this.

I meant this pep:

1 Like

I think that it is generally good to bring the syntax of lambdas in line with that of named function definitions by allowing parenthesized parameter lists, type parameters and return type annotations. So I am +1 on that.

However, I think the impact is not as great as the OP suggests since the type checkers are supposed to infer the types of the lambda’s parameters and by extension its return value. Inferring types depends on the context. So, what can I do with a lambda?

  1. Assigning the lambda to a variable. Although such occurrences can be replaced by named functions, annotations are possible, like in func: Callable[[int],int] = lambda x: -x.
  2. Returning the lambda from a function. In this cases, types can be inferred from the return type annotation.
  3. Immediately invoking the lambda. In Python, I am not aware of use cases for doing that, but the parameter’s types can be inferred from the argument’s types.
  4. The most important case is passing a lambda to a function, like the builtin sort function. In typeshed it is annotated with def sort(self, *, key: Callable[[_T], SupportsRichComparison], reverse: bool = False) -> None: .... So, the program
def foo() -> None:
   l: list[str] = ["a","b","c"]
   l.sort(key=lambda v: -v)

is rejected by mypy with the perfectly valid reason

test.py:3: error: Unsupported operand type for unary - ("str")  [operator]
Found 1 error in 1 file (checked 1 source file)

So, mypy inferred without type annotations that v is supposed to be a str object. While the type checkers may be not perfect yet, they certainly do a great job.

1 Like

Thank you for such a detailed response! Your examples regarding sort and simple type inference are very convincing and certainly cover the majority of basic use cases.

However, I would like to add a few complementary perspectives drawn from engineering practice and the edge cases of the type system. Beyond simple behavior encapsulation, lambdas are often used as a tool for logical cohesion, helping to avoid unnecessary namespace pollution. Regarding the proposed syntax changes, here are a few thoughts:

  1. The boundaries of type inference: While simple contextual inference works well, it often falls short in complex scenarios involving Generics, Overloads, and Contravariance/Covariance. This is especially true when the target function expects Any or when the context is ambiguous. In these cases, explicit parameter annotations are crucial as they serve as the starting point for type inference, rather than relying solely on the external context as the endpoint. Furthermore, while assigning a lambda to a variable allows for annotations, this pattern is discouraged by PEP 8, so native support within the lambda itself would be a much more elegant solution.
  2. The utility of IIFEs: Immediately Invoked Function Expressions (IIFEs) using lambdas are actually quite valuable for complex constant initialization or creating clean, private scopes. In such scenarios, without explicit annotations, type checkers often struggle to correctly capture the developer’s intent.

Explicit parameter annotations should serve as the source of truth that drives type inference, rather than being a derivative of the context. Therefore, relying solely on contextual typing for formal parameters is often fragile.

In summary, explicit syntax would better bridge the gap in type safety for these more complex scenarios.

Are there any reasonable use cases in Python where it would be preferable to use an IIFE lambda over a nested function?

1 Like

Actually, Python does not generally encourage the use of IIFE, but there are still some scenarios where it is necessary or useful:

# Loop Variable Binding (Closure Capture)
funcs = [(lambda x: lambda: x)(i) for i in range(5)]

# Use closures for value storage while keeping scope isolated.
time_used = (lambda start: time.time() - start)(time.time())

This is the kind of weird code I expect from an LLM. As is putting more effort into formatting than actual content. Are you getting an LLM to write your posts for you?

5 Likes

I use LLMs for translation, including comments.

sigh So once again, I’ve been wasting time giving someone the benefit of the doubt as to whether the post was “polished” by an LLM.

I can’t find the forum rules where it’s stated, but wasn’t it a violation of those rules to use an LLM and not say so?

But, better, just don’t. Don’t use LLMs to “fix” or “improve” your posts. They invariably end up making the post worse. Sure, it might be nicely formatted, but it’s a nicely formatted piece of AI garbage. How am I supposed to judge your point about IIFEs when the entire post could just have come from an LLM’s knowledge of JavaScript?

I’m done here. Discussing with an LLM isn’t what I come to this forum for.

First, the use of IIFE is not the main focus of this idea.
Second, IIFEs are generally not encouraged in Python, just a code style.
Finally, if you feel this topic isn’t worth discussing, please feel free to not participate.

1 Like

Then use a translator (like deepl.com) not an LLM.

1 Like

Yeah, good idea.