ruff/crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md
Dhruv Manilawala da069aa00c
[red-knot] Infer lambda expression (#16547)
## Summary

Part of https://github.com/astral-sh/ruff/issues/15382

This PR adds support for inferring the `lambda` expression and return
the `CallableType`.

Currently, this is only limited to inferring the parameters and a todo
type for the return type.

For posterity, I tried using the `file_expression_type` to infer the
return type of lambda but it would always lead to cycle. The main reason
is that in `infer_parameter_definition`, the default expression is being
inferred using `file_expression_type`, which is correct, but it then

Take the following source code as an example:
```py
lambda x=1: x
```

Here's how the code will flow:
* `infer_scope_types` for the global scope
* `infer_lambda_expression`
* `infer_expression` for the default value `1`
* `file_expression_type` for the return type using the body expression.
This is because the body creates it's own scope
* `infer_scope_types` (lambda body scope)
* `infer_name_load` for the symbol `x` whose visible binding is the
lambda parameter `x`
* `infer_parameter_definition` for parameter `x`
* `file_expression_type` for the default value `1`
* `infer_scope_types` for the global scope because of the default
expression

This will then reach to `infer_definition` for the parameter `x` again
which then creates the cycle.

## Test Plan

Add tests around `lambda` expression inference.
2025-03-11 11:25:20 +05:30

2.4 KiB

lambda expression

No parameters

lambda expressions can be defined without any parameters.

reveal_type(lambda: 1)  # revealed: () -> @Todo(lambda return type)

# error: [unresolved-reference]
reveal_type(lambda: a)  # revealed: () -> @Todo(lambda return type)

With parameters

Unlike parameters in function definition, the parameters in a lambda expression cannot be annotated.

reveal_type(lambda a: a)  # revealed: (a) -> @Todo(lambda return type)
reveal_type(lambda a, b: a + b)  # revealed: (a, b) -> @Todo(lambda return type)

But, it can have default values:

reveal_type(lambda a=1: a)  # revealed: (a=Literal[1]) -> @Todo(lambda return type)
reveal_type(lambda a, b=2: a)  # revealed: (a, b=Literal[2]) -> @Todo(lambda return type)

And, positional-only parameters:

reveal_type(lambda a, b, /, c: c)  # revealed: (a, b, /, c) -> @Todo(lambda return type)

And, keyword-only parameters:

reveal_type(lambda a, *, b=2, c: b)  # revealed: (a, *, b=Literal[2], c) -> @Todo(lambda return type)

And, variadic parameter:

reveal_type(lambda *args: args)  # revealed: (*args) -> @Todo(lambda return type)

And, keyword-varidic parameter:

reveal_type(lambda **kwargs: kwargs)  # revealed: (**kwargs) -> @Todo(lambda return type)

Mixing all of them together:

# revealed: (a, b, /, c=Literal[True], *args, *, d=Literal["default"], e=Literal[5], **kwargs) -> @Todo(lambda return type)
reveal_type(lambda a, b, /, c=True, *args, d="default", e=5, **kwargs: None)

Parameter type

In addition to correctly inferring the lambda expression, the parameters should also be inferred correctly.

Using a parameter with no default value:

lambda x: reveal_type(x)  # revealed: Unknown

Using a parameter with default value:

lambda x=1: reveal_type(x)  # revealed: Unknown | Literal[1]

Using a variadic paramter:

# TODO: should be `tuple[Unknown, ...]` (needs generics)
lambda *args: reveal_type(args)  # revealed: tuple

Using a keyword-varidic parameter:

# TODO: should be `dict[str, Unknown]` (needs generics)
lambda **kwargs: reveal_type(kwargs)  # revealed: dict

Nested lambda expressions

Here, a lambda expression is used as the default value for a parameter in another lambda expression.

reveal_type(lambda a=lambda x, y: 0: 2)  # revealed: (a=(x, y) -> @Todo(lambda return type)) -> @Todo(lambda return type)