Skip to content

[ty] Avoid duplicate bindings in multi-target assignments - #27938

Merged
charliermarsh merged 5 commits into
mainfrom
charlie/fix-chained-assignment-lambda
Aug 22, 2026
Merged

charliermarsh merged 5 commits into
mainfrom
charlie/fix-chained-assignment-lambda

Conversation

@charliermarsh

@charliermarsh charliermarsh commented Aug 20, 2026 •

Copy link
Copy Markdown
Member

Summary

Previously, chained assignments could panic when their shared right-hand side contained both an assignment expression and a lambda:

first = second = (named := lambda: 0)

Each target independently imported the same nested binding, violating the uniqueness invariant for inferred bindings.

We now record whether an assignment's value bindings belong to its definition or its enclosing statement during semantic indexing. Statements claim shared bindings once while individual targets retain contextual expression inference. The same ownership rule handles unpacking, subscript targets, and assignment expressions in lambda defaults:

first = second = lambda value=(named := 1): value

@astral-sh-bot astral-sh-bot Bot added the ty The ty type checker label Aug 20, 2026
@astral-sh-bot

astral-sh-bot Bot commented Aug 20, 2026 •

Copy link
Copy Markdown

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 97.69%. The percentage of expected errors that received a diagnostic held steady at 93.71%. The number of fully passing files held steady at 110/136.

@astral-sh-bot

astral-sh-bot Bot commented Aug 20, 2026 •

Copy link
Copy Markdown

Memory usage report

Summary

Project Old New Diff Outcome
prefect 475.65MB 475.73MB +0.02% (81.40kB) ⏫
sphinx 171.98MB 172.02MB +0.02% (39.34kB) ⏫
trio 96.75MB 96.75MB +0.00% (2.06kB) ⏫
flake8 40.14MB 40.14MB +0.00% (1.42kB) ⏫

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
infer_expression_types_impl 24.42MB 24.50MB +0.30% (74.80kB) ⏫
infer_statement_types_impl 673.12kB 676.44kB +0.49% (3.31kB) ⏫
infer_scope_types_impl 19.11MB 19.11MB +0.01% (1.55kB) ⏫
infer_definition_types 36.96MB 36.96MB +0.00% (1.06kB) ⏫
CallableType 8.55MB 8.55MB +0.01% (496.00B) ⏫
all_narrowing_constraints_for_expression 4.65MB 4.65MB +0.00% (144.00B) ⏫
analyze_non_terminal_call 1.03MB 1.03MB +0.00% (40.00B) ⏫
infer_unpack_types 448.42kB 448.43kB +0.00% (8.00B) ⏫

sphinx

Name Old New Diff Outcome
infer_expression_types_impl 11.98MB 12.02MB +0.28% (34.85kB) ⏫
CallableType 1.67MB 1.67MB +0.08% (1.28kB) ⏫
infer_statement_types_impl 528.34kB 529.18kB +0.16% (856.00B) ⏫
infer_scope_types_impl 6.13MB 6.13MB +0.01% (784.00B) ⏫
infer_definition_types 11.61MB 11.61MB +0.00% (424.00B) ⏫
Type<'db>::apply_specialization_inner_::interned_arguments 1.82MB 1.82MB +0.02% (320.00B) ⏫
Type<'db>::apply_specialization_inner_ 1.16MB 1.16MB +0.02% (192.00B) ⏫
TypePair 2.18MB 2.18MB +0.01% (192.00B) ⏫
IntersectionType 631.23kB 631.39kB +0.03% (168.00B) ⏫
UnionType 627.86kB 627.97kB +0.02% (112.00B) ⏫
all_narrowing_constraints_for_expression 2.38MB 2.38MB +0.00% (80.00B) ⏫
is_redundant_with_impl 792.92kB 793.00kB +0.01% (80.00B) ⏫
member_lookup_with_policy_and_receiver_inner 1.65MB 1.65MB +0.00% (16.00B) ⏫
member_lookup_with_policy_inner 3.69MB 3.69MB +0.00% (16.00B) ⏫
infer_expression_type_impl 289.47kB 289.48kB +0.01% (16.00B) ⏫
... 2 more

trio

Name Old New Diff Outcome
infer_expression_types_impl 6.43MB 6.43MB +0.03% (2.03kB) ⏫
infer_definition_types 4.73MB 4.73MB +0.00% (16.00B) ⏫
infer_statement_types_impl 78.42kB 78.44kB +0.02% (16.00B) ⏫

flake8

Name Old New Diff Outcome
infer_expression_types_impl 804.29kB 805.55kB +0.16% (1.27kB) ⏫
infer_definition_types 1.12MB 1.12MB +0.01% (64.00B) ⏫
infer_statement_types_impl 32.13kB 32.18kB +0.17% (56.00B) ⏫
member_lookup_with_policy_and_receiver_inner 55.20kB 55.21kB +0.01% (8.00B) ⏫
member_lookup_with_policy_inner 292.34kB 292.34kB +0.00% (8.00B) ⏫
infer_expression_type_impl 21.28kB 21.29kB +0.04% (8.00B) ⏫
StaticClassLiteral<'db>::implicit_attribute_inner_ 55.52kB 55.52kB +0.01% (8.00B) ⏫

@astral-sh-bot

astral-sh-bot Bot commented Aug 20, 2026 •

Copy link
Copy Markdown

ecosystem-analyzer results

No diagnostic changes detected ✅

Full report with detailed diff (timing results)

@charliermarsh
charliermarsh marked this pull request as ready for review August 21, 2026 17:45
@charliermarsh
charliermarsh requested review from a team as code owners August 21, 2026 17:45
@charliermarsh charliermarsh added the bug An issue describing something that isn't working, or a PR that fixes a bug label Aug 21, 2026
@astral-sh-bot
astral-sh-bot Bot requested a review from dhruvmanila August 21, 2026 17:45
@carljm
carljm requested review from carljm and removed request for dhruvmanila August 22, 2026 05:15
from collections.abc import Callable

first: Callable[[int], int]
second: Callable[[int], int]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we have this case use different annotations on the two targets? With identical annotations, this test would still pass if we accidentally reused the first target's inferred callable type for both.

For example:

first: Callable[[int], int]
second: Callable[[str], int]
first = second = lambda value: 0

reveal_type(first)  # revealed: (value: int) -> Literal[0]
reveal_type(second)  # revealed: (value: str) -> Literal[0]

Comment on lines +24 to +33

The same applies when the assignment expression and lambda are separate parts of the shared value.

```py
first = second = ((named := 0), lambda: 1)

reveal_type(first) # revealed: tuple[Literal[0], () -> Literal[1]]
reveal_type(second) # revealed: tuple[Literal[0], () -> Literal[1]]
reveal_type(named) # revealed: Literal[0]
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we can collapse this with the test immediately below, which uses the same RHS form.

Ok(())
}

#[test]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't really think it's worth adding Rust unit tests for this. I can't see what observable failure would be caused by failing to store this metadata correctly, that wouldn't cause the mdtests to fail.

I'm not usually one to turn down test coverage, but overall my feeling is that this PR is over-tested for the very niche edge case it handles.

@charliermarsh
charliermarsh force-pushed the charlie/fix-chained-assignment-lambda branch from 5ec3806 to 9b62740 Compare August 22, 2026 12:37
@charliermarsh
charliermarsh enabled auto-merge (squash) August 22, 2026 12:37
@charliermarsh
charliermarsh merged commit f42a9e0 into main Aug 22, 2026
66 checks passed
@charliermarsh
charliermarsh deleted the charlie/fix-chained-assignment-lambda branch August 22, 2026 12:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug An issue describing something that isn't working, or a PR that fixes a bug ty The ty type checker

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants