Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions packages/pyright-internal/src/analyzer/typeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15264,25 +15264,50 @@ export function createTypeEvaluator(
// more sophisticated in the future, but it becomes very complex to handle
// all of the permutations.
let sawParamMismatch = false;
let expectedParamIndex = 0;
const positionOnlySeparatorIndex = node.d.params.findIndex(
(param) => param.d.category === ParamCategory.Simple && !param.d.name
);

node.d.params.forEach((param, index) => {
let paramType: Type | undefined;

if (expectedParamDetails && !sawParamMismatch) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue · Please address or respond

Can the unnamed bare * be normalized separately from the contextual parameter index? If getParamListDetails omits that separator, lambda *, value: ... compares * with the expected value parameter, marks a mismatch, and never contextually types value. Please skip or align the separator and add this regression case.

if (index < expectedParamDetails.params.length) {
const expectedParam = expectedParamDetails.params[index];
const isKeywordOnlySeparator = param.d.category === ParamCategory.ArgsList && !param.d.name;

if (isKeywordOnlySeparator) {
if (
expectedParamIndex < expectedParamDetails.params.length &&

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

A bare * appears to consume an index even though contextual parameter details omit separators, potentially leaving lambda *, value: ... without the contextual type for value. Add that protocol scenario with assert_type(value, int) and, if confirmed, track lambda and contextual indexes separately.

expectedParamDetails.params[expectedParamIndex].kind !== ParamKind.Keyword

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

This guard also prevents contextual typing for a compatible single callable such as Callable[*, value: int] assigned from lambda value: value: that lambda accepts the required value= keyword, but its parameter now falls back to Unknown. Please preserve contextual typing when the lambda parameter name matches the keyword-only expected parameter, or add coverage showing why this case must be rejected.

[verified]

) {
sawParamMismatch = true;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

Once sawLambdaArgsParam is true, any keyword-only contextual parameter is accepted regardless of its name, so lambda *args, other: ... may inherit the type of contextual value. Add a callable-union regression using assert_type(other, ...); require matching names after *args if the candidate can otherwise influence inference.

} else if (expectedParamIndex < expectedParamDetails.params.length) {
const expectedParam = expectedParamDetails.params[expectedParamIndex];
const isPositionOnlyParam =
(positionOnlySeparatorIndex >= 0 && index < positionOnlySeparatorIndex) ||
(positionOnlySeparatorIndex < 0 &&

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue · Please address or respond

Can this continue to require matching keyword names after *args? sawLambdaArgsParam currently accepts every following keyword-only expected parameter regardless of name, but lambda *args, value: ... does not accept other=.... This can let an incompatible union candidate provide contextual types; retain the name check and add a differing-keyword-name regression.

paramsArePositionOnly &&
!!param.d.name &&
isPrivateName(param.d.name.d.value));
const isCompatibleKeywordParam =
expectedParam.kind !== ParamKind.Keyword ||

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Info · Optional note

📍 packages/pyright-internal/src/analyzer/typeEvaluator.ts:15289
Prior feedback remains: positional-only classification still depends partly on paramsArePositionOnly, which is mutated during iteration. Consider precomputing each lambda parameter's positional-only status to remove this low-risk ordering dependency.

[verified]

(!isPositionOnlyParam && param.d.name?.d.value === expectedParam.param.name);

// If the parameter category matches and both of the parameters are
// either separators (/ or *) or not separators, copy the type

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

isPositionOnlyParam depends on paramsArePositionOnly, which is mutated during iteration and makes matching sensitive to loop ordering. Precompute each lambda parameter's positional-only status before contextual matching.

[verified]

// from the expected parameter.
if (
expectedParam.param.category === param.d.category &&
!param.d.name === !expectedParam.param.name
!param.d.name === !expectedParam.param.name &&
isCompatibleKeywordParam
) {
paramType = expectedParam.type;
} else {
sawParamMismatch = true;
}

expectedParamIndex++;
} else if (param.d.defaultValue) {
// If the lambda param has a default value but there is no associated
// parameter in the expected type, assume that the default value is
Expand Down
62 changes: 61 additions & 1 deletion packages/pyright-internal/src/tests/samples/lambda4.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This sample tests the case where a lambda is assigned to
# a union type that contains multiple callables.

from typing import Callable, Protocol, TypeVar
from typing import Callable, Generic, Protocol, Self, TypeVar, assert_type


U1 = Callable[[int, str], bool] | Callable[[str], bool]
Expand Down Expand Up @@ -76,3 +76,63 @@ def accepts_u2(cb: U2) -> U2:
def accepts_u3(u: U3):
# This should generate an error.
u(lambda v: v.lower())


class KeywordOnlyCallable:
def __call__(self, *, kwarg: int) -> Self: ...


keyword_only_union: Callable[[KeywordOnlyCallable], KeywordOnlyCallable] | KeywordOnlyCallable = lambda x: x


class GenericKeywordOnlyCallable(Generic[T]):
def __call__(self, *, kwarg: T) -> Self: ...


generic_keyword_only_union: (
Callable[[GenericKeywordOnlyCallable[int]], GenericKeywordOnlyCallable[int]] | GenericKeywordOnlyCallable[int]
) = lambda x: x


class KeywordOnlyCallback(Protocol):
def __call__(self, *, value: int) -> Self: ...


protocol_keyword_only_union: Callable[[KeywordOnlyCallback], KeywordOnlyCallback] | KeywordOnlyCallback = lambda x: x

ordinary_callable_union: Callable[[int], int] | Callable[[str], str] = lambda x: x


class PositionalCallable:
def __call__(self, value: int) -> Self: ...


positional_callable_union: Callable[[PositionalCallable], PositionalCallable] | PositionalCallable = lambda x: x

# This should generate an error.
keyword_only_callback: KeywordOnlyCallback = lambda x: x

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

Add a positive regression case for the enabled branch: a lambda with *args followed by a keyword-only parameter contextually typed against a keyword-only callable. The new guard's rejection behavior is well covered, but this ensures valid keyword-only lambda parameters still receive their expected type.

[verified]



class KeywordOnlyIntCallback(Protocol):
def __call__(self, *, value: int) -> int: ...


same_name_keyword_only_callback: KeywordOnlyIntCallback = lambda value: assert_type(value, int)


class VariadicKeywordOnlyCallback(Protocol):
def __call__(self, *args: object, value: int) -> int: ...


variadic_keyword_only_callback: VariadicKeywordOnlyCallback = lambda *args, value: assert_type(value, int)

# The bare `*` separator should not consume the contextual parameter index.
bare_keyword_only_callback: KeywordOnlyIntCallback = lambda *, value: assert_type(value, int)

# This should generate an error because the keyword-only parameter name differs.
variadic_keyword_only_callback_different_name: VariadicKeywordOnlyCallback = lambda *args, other: reveal_type(
other, expected_text="Unknown"
)

# This should generate an error.
position_only_keyword_callback: KeywordOnlyIntCallback = lambda value, /: value
2 changes: 1 addition & 1 deletion packages/pyright-internal/src/tests/typeEvaluator1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@ test('Lambda3', () => {
test('Lambda4', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['lambda4.py']);

TestUtils.validateResults(analysisResults, 2);
TestUtils.validateResults(analysisResults, 5);
});

test('Lambda5', () => {
Expand Down
Loading