Typed Anonymous Functions (lamdef)

Following Łukasz’s inquiry about multi-line lambda at the EuroPython 2025 Core Dev Panel (and revisiting longstanding threads like “Are better lambdas impossible?”). I would like to propose a viable solution.

The Motivation of lamdef: It’s about Type Safety, not just Multi-line

While lambda serves us well for simple expressions, it creates a gap in modern, typed codebases.

I have drafted a Pre-PEP (lamdef/pep-tbd.rst at main · note35/lamdef · GitHub) for a new soft keyword, lamdef. It introduces a typed, multi-statement anonymous function syntax that leverages the Off-side Rule to solve the parsing ambiguity. For example:

df['status'] = df.apply(lamdef(row: Series) -> str:
    score = row['score']
    if score > 90:
        return "Premium"
    if score > 50:
        return "Active"
    return "Inactive"
, axis=1)

See “The Readability Gap” in the Pre-PEP for more common examples.

Proof of Concept

This is not just a theoretical idea. I have a working prototype based on CPython 3.15 (https://github.com/gkirchou/cpython/tree/lamdef) that passes the parser logic.

I am looking for feedback on the grammar choices (specifically the strict indentation rules for container delimiters) and any potential edge cases I might have missed.

Thanks!

5 Likes

How does this address any of the concerns voiced previously? No, I am not going to repeat the concerns, please do your own research.

Dear Kir Chou, welcome to this forum! As @ MegaIng arleady mentioned, there have been many discussions over the years about extending lambdas so far that would need to be addressed. Most of them are on readability, which is arguably always a bit subjective. However I have to points that are specific to your proposal:

1. The proposal conflates typing with multi-line. In the abstract you want to make it look as if your proposal is about allowing type annotations for lambdas. However, it does not provide type annotations for lambdas, but multiline lamdef functions that can be annotated.

So far, lambdas cannot be annotated, it is expected that the type checker infer types. I tried mypy and it can do it

from typing import Callable
def spam(x:Callable[[int],int]): print(x(42))
spam(lambda x:x+1) #OK
spam(lambda x:x+1.0) # error: Argument 1 to "spam" has incompatible type "Callable[[int], float]"; expected "Callable[[int], int]" [arg-type]

If, for whatever reason, I want to be explicit about typing, your proposal (please correct me if I misread you grammar specs) would require 3 lines to write the function call:

spam(lamdef (x:int) -> int:
    return x+1
)

However, I can already do it in 2 lines:

def _addone(x:int) -> int: return x+1
spam(_addone)

2. The proposal does not specify the lexer changes. You write a lot about the parser, but the PEG grammar is quite straight-forward. However, implicit line continuation is realized in the lexer by making it not emit NEWLINE and INDENT or DEDENT tokens. As you know (you have implemented it!), the lexer has to be changed for your proposal to work. You only shortly mention it in the section “Performance Challenge in Lexer”. However, these changes are not just performance challenges or implementation details, but essential parts of the language specs. Unfortunately, there is no formal specification of the lexer, however clearly describing the rules of lexing is as important or even more important than the rules of the PEG grammar.

2 Likes

And as a short bonus, I also disagree with your FAQ section “Why not extend lambda to support type hints?

If you desire to have something like this:
f = lambda (x: int, y: int): x + y
PEP 3113 removes Tuple Parameter Unpacking is one of the strongest reason:

No, quite the opposite: Since PEP 3113 removed Tuple Parameter Unpacking, one could now reuse that Python 2 syntax to allow lambda expression parameters to be parenthesized.

And if you desire to have something like this (no space between lambda and ():
a = lambda(x: int, y: int): x + y
This would allow lambda to have two ways (lambda(x): vs lambda x:) to do the same thing, which violates the Zen of Python: There should be one-- and preferably only one --obvious way to do it.

  1. Space or no space. There is no difference.
  2. The Zen of Python does not say that there must not be more than one way to do it. And, anyway, lamdef would be a second way as well…
5 Likes

Thanks for the feedback @Stefan!

I have made two major updates to the Pre-PEP based on your suggestions.

  1. Added a Lexer section to the Detailed Specification.
  2. Revised the content in “Why not extend lambda to support type hints?”.

For other points you raised:

1. Type Inference limits: Inference works for trivial cases but breaks down when data sources are untyped (e.g., json.loads returns Any). There is a fundamental difference between choosing not to annotate (inference) and being unable to annotate (current lambda).

import json
from typing import Any, reveal_type 

users = json.loads('[{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]')
reveal_type(users)  # Mypy: note: Revealed type is "Any"

# lambda
# mypy failed to detect to_xml is not defined
list(map(lambda u: u.to_xml(), users))

# def
# error: "dict[str, Any]" has no attribute "to_xml"
def process_user(u: dict[str, Any]):
    return u.to_xml()
list(map(process_user, users))  

# lamdef
# error: "dict[str, Any]" has no attribute "to_xml"
list(map(lamdef(u: dict[str, Any]):
    return u.to_xml()
, users))

2. Locality vs. Line count: The primary motivation is Code Locality, not vertical compactness. Even a short def forces a context switch (jumping to read the definition) and pollutes the namespace with disposable names (e.g., process_user in above example). lamdef keeps the logic explicitly where it is consumed.

3. Typed lambda: I agree that the PEP 3113 argument was weak and I have revised it. The introduction of lamdef is primarily driven by the need for multi-line statements, a scope distinct from simply adding types to lambda. To prevent semantic ambiguity (having two ways to do the same thing), I intend to keep lamdef strictly for multi-line blocks. This reasoning is further elaborated in the section Why is Single-line Lamdef currently reserved (banned)?.

3 Likes

That is how it should work.

users is, how you already said, of type Any (for type-checkers).

If I were to translate this code in a way it’s easier to see what is going on, this is what we have:

from typing import Any, reveal_type
import json

users1: Any = json.loads(...)
reveal_type(users1) # Any

list(map(lambda u: u.to_xml(), users1))

users2: list[dict[str, Any]= json.loads(...)
reveal_type(users2) # list[dict[str, Any]]

list(map(lambda u: u.to_xml(), users2))

So Mypy does complain, of we are fair. But in your example, you treat lambda differently. It gets a u: Any, so the attribute access of u.to_xml() is valid. If you had given it u: dict[str, Any], it would complain too.

But if functions themselves also can already do everything lambdef callables can, why not use them?

Sure, you’d have to define them earlier, and you also need a name, but that’s all. No need to introduce a new (soft) keyword.

As for the typing aspect, another problem I see is the inspection. I was working on a project where normal functions, and lambdas (and class constructors) could be passed, and I had to figure out what parameters existed, and what their type is, as well as the return type of the callable. The code would have been way simpler, if lambdas didn’t exist. Now adding yet another kind of callable only makes things harder for anyone trying to accept multiple kinds of callables for some inspection or similar.

2 Likes

Thanks for the perspective!

1. Handling Untyped Data: You are absolutely right! If the variable is explicitly typed (e.g., users2), lambda works perfectly. However, the json.loads example I provided highlights this gap: when we consume untyped data (which returns Any), lamdef allows us to enforce a contract at the usage site as def, whereas lambda silently propagates the Any and misses the error (e.g., list(map(lambda u: u.to_xml(), json.loads(…))).

2. Inspection: To address your concern about tooling overhead, lamdef does not introduce a new kind of callable at runtime. It compiles down to the exact same standard function type as lambda (type(f) is type(lambda: None), see Type System in pre-PEP). Since it natively populates standard attributes like __annotations__, existing inspection tools will work out-of-the-box, treating it just like any other function.

3. Why not just use def? def is equivalent. lamdef simply offers an alternative for code locality and avoiding the mental overhead of naming one-off functions.

If I have misinterpreted any of your points, please correct me! Thanks.

1 Like

A reminder that the TOS of this website explicitly ban copy-pasting LLM generated output, especially without explicitly declaring it.

If you are not a native speaker it’s better to have slightly broken english then pushing your answers through LLM, which makes it more difficult to make sure you, the person, actually understand what we are saying.

6 Likes

Got it, if replying normally is prefered I can do it (without LLM’s revise I mean), hopefully not editing too much, I am trying not to make too many noises here.

1 Like

I mean if you already go through the trouble of using a LLM, you could just as well use a website like https://deepl.com to translate your text. It will stick much closer to the original meaning.

1 Like

For anyone interested in what the panel said about it, you can watch it here (timestamp 43:08): https://www.youtube.com/watch?v=0j8euKVjirg&t=2588s

2 Likes