Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[ty] Infer types for names bound in sequence match patterns#25940

Merged
charliermarsh merged 22 commits into
mainfrom
charlie/pattern-as
Jun 23, 2026
Merged

[ty] Infer types for names bound in sequence match patterns#25940
charliermarsh merged 22 commits into
mainfrom
charlie/pattern-as

Conversation

@charliermarsh

@charliermarsh charliermarsh commented Jun 13, 2026

Copy link
Copy Markdown
Member

Summary

This PR infers the types of names introduced by match patterns.

def unpack(value: tuple[int, str]) -> None:
    match value:
        case [first, second] as whole:
            reveal_type(first)   # int
            reveal_type(second)  # str
            reveal_type(whole)   # tuple[int, str]

The implementation analyzes a successful match recursively: each pattern receives the type of the value it is matching, computes the type that can successfully match, and passes the appropriate extracted type to its children. A capture binds the type it receives; an as pattern binds the successful type of its complete nested pattern.

The analysis also distinguishes facts established while a pattern is being evaluated from the stable type assigned to an alias. An alias for a mutable sequence keeps only its sequence type, because later mutation can invalidate the observed shape; exact tuples can retain facts learned from their children. For example, we don't narrow here:

def f(value: list[int | str]) -> None:
    match value:
        case [int(), str()] as whole:
            reveal_type(whole)       # list[int | str]
            reveal_type(len(whole))  # int, not Literal[2]
            reveal_type(whole[0])    # int | str, not int
            reveal_type(whole[1])    # int | str, not str

But we do narrow for an exact tuple:

def f(value: tuple[int | str, int | str]) -> None:
    match value:
        case [int(), str()] as whole:
            reveal_type(len(whole))  # Literal[2]
            reveal_type(whole[0])    # int
            reveal_type(whole[1])    # str

@charliermarsh charliermarsh added the ty Multi-file analysis & type inference label Jun 13, 2026
@astral-sh-bot

astral-sh-bot Bot commented Jun 13, 2026

Copy link
Copy Markdown

Typing conformance results

The percentage of diagnostics emitted that were expected errors held steady at 94.37%. The percentage of expected errors that received a diagnostic held steady at 89.00%. The number of fully passing files held steady at 94/134.

Summary

How are test cases classified?

Each test case represents one expected error annotation or a group of annotations sharing a tag. Counts are per test case, not per diagnostic — multiple diagnostics on the same line count as one. Required annotations (E) are true positives when ty flags the expected location and false negatives when it does not. Optional annotations (E?) are true positives when flagged but true negatives (not false negatives) when not. Tagged annotations (E[tag]) require ty to flag exactly one of the tagged lines; tagged multi-annotations (E[tag+]) allow any number up to the tag count. Flagging unexpected locations counts as a false positive.

Metric Old New Diff Outcome
True Positives 955 955 +0
False Positives 57 57 +0
False Negatives 118 118 +0
Total Diagnostics 1060 1060 +0
Precision 94.37% 94.37% +0.00%
Recall 89.00% 89.00% +0.00%
Passing Files 94/134 94/134 +0

True positives changed (2)

2 diagnostics
Test case Diff

tuples_type_compat.py:106

-error[type-assertion-failure] Type `tuple[str, str] | (tuple[int, *tuple[str, ...], int] & <Protocol with members '__getitem__', '__len__'> & ~<Protocol with members '__getitem__', '__len__'>)` does not match asserted type `tuple[str, str] | tuple[int, int]`
+error[type-assertion-failure] Type `tuple[str, str] | (tuple[int, *tuple[str, ...], int] & <Protocol with members '__getitem__', '__len__'>)` does not match asserted type `tuple[str, str] | tuple[int, int]`

tuples_type_compat.py:111

-error[type-assertion-failure] Type `tuple[int, *tuple[str, ...], int] & <Protocol with members '__getitem__', '__len__'> & ~<Protocol with members '__getitem__', '__len__'> & ~<Protocol with members '__getitem__', '__len__'>` does not match asserted type `tuple[int, str, int]`
+error[type-assertion-failure] Type `tuple[int, *tuple[str, ...], int] & <Protocol with members '__getitem__', '__len__'>` does not match asserted type `tuple[int, str, int]`

@astral-sh-bot

astral-sh-bot Bot commented Jun 13, 2026

Copy link
Copy Markdown

Memory usage report

Memory usage unchanged ✅

@astral-sh-bot

astral-sh-bot Bot commented Jun 13, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-argument-type 13 0 0
invalid-assignment 1 0 3
type-assertion-failure 0 0 3
unresolved-attribute 1 0 1
invalid-return-type 1 0 0
Total 16 0 7

Large timing changes:

Project Old Time New Time Change
egglog-python 0.20s 0.31s +54%

Flaky changes detected. This PR summary excludes flaky changes; see the HTML report for details.

Raw diff (23 changes)
cibuildwheel (https://github.com/pypa/cibuildwheel)
+ cibuildwheel/options.py:428:50 error[invalid-argument-type] Argument to bound method `OptionFormat.format_list` is incorrect: Expected `Sequence[str | int]`, found `(Mapping[str, Sequence[str | int] | int] & Sequence[object] & ~str & ~bytes & ~bytearray) | (Sequence[str | int] & ~str & ~bytes & ~bytearray) | (int & Sequence[object])`

cwltool (https://github.com/common-workflow-language/cwltool)
+ cwltool/secrets.py:46:40 error[invalid-argument-type] Argument to bound method `SecretStore.has_secret` is incorrect: Expected `int | str | float | ... omitted 4 union elements`, found `object`
+ cwltool/secrets.py:50:40 error[invalid-argument-type] Argument to bound method `SecretStore.has_secret` is incorrect: Expected `int | str | float | ... omitted 4 union elements`, found `object`
+ cwltool/secrets.py:62:24 error[invalid-return-type] Return type does not match returned value: expected `int | str | float | ... omitted 4 union elements`, found `dict[object, int | str | float | ... omitted 4 union elements]`
+ cwltool/secrets.py:62:42 error[invalid-argument-type] Argument to bound method `SecretStore.retrieve` is incorrect: Expected `int | str | float | ... omitted 4 union elements`, found `object`
+ cwltool/secrets.py:64:39 error[invalid-argument-type] Argument to bound method `SecretStore.retrieve` is incorrect: Expected `int | str | float | ... omitted 4 union elements`, found `object`
+ cwltool/process.py:507:44 error[invalid-argument-type] Argument to function `var_spool_cwl_detector` is incorrect: Expected `int | str | float | ... omitted 5 union elements`, found `object`
+ cwltool/process.py:510:44 error[invalid-argument-type] Argument to function `var_spool_cwl_detector` is incorrect: Expected `int | str | float | ... omitted 5 union elements`, found `object`

egglog-python (https://github.com/egraphs-good/egglog-python)
- python/egglog/egraph_state.py:378:25 error[type-assertion-failure] Type `@Todo & ~Literal["delete"] & ~Literal["subsume"]` is not equivalent to `Never`
+ python/egglog/egraph_state.py:378:25 error[type-assertion-failure] Type `Unknown & ~Literal["delete"] & ~Literal["subsume"]` is not equivalent to `Never`
- python/egglog/egraph_state.py:623:25 error[type-assertion-failure] Type `@Todo & ~None & ~int & ~float & ~str` is not equivalent to `Never`
+ python/egglog/egraph_state.py:623:25 error[type-assertion-failure] Type `Unknown & ~None & ~int & ~float & ~str` is not equivalent to `Never`
- python/egglog/pretty.py:309:17 error[type-assertion-failure] Type `@Todo & ~None & ~int & ~float & ~str` is not equivalent to `Never`
+ python/egglog/pretty.py:309:17 error[type-assertion-failure] Type `Unknown & ~None & ~int & ~float & ~str` is not equivalent to `Never`

jax (https://github.com/google/jax)
+ jax/_src/pallas/mosaic_gpu/lowering.py:1666:55 error[invalid-argument-type] Argument to bound method `UnswizzleRef.commute_reshape` is incorrect: Expected `ShapedArray`, found `AbstractValue`
+ jax/experimental/mosaic/gpu/layout_inference.py:424:10 error[invalid-assignment] Object of type `WGStridedFragLayout & ~WGSplatFragLayout` is not assignable to `RegisterLayout`
+ jax/experimental/mosaic/gpu/layout_inference.py:425:54 error[unresolved-attribute] Object of type `RegisterLayout` has no attribute `vec_size`
+ jax/experimental/mosaic/gpu/layout_inference.py:426:51 error[invalid-argument-type] Argument to function `is_supported_strided_layout_broadcast` is incorrect: Expected `WGStridedFragLayout`, found `RegisterLayout`

kopf (https://github.com/nolar/kopf)
- kopf/_cogs/structs/dicts.py:244:29 error[unresolved-attribute] Object of type `(_T@walk & ~None) | Iterable[_T@walk | Iterable[_T@walk]]` has no attribute `obj`
+ kopf/_cogs/structs/dicts.py:244:29 error[unresolved-attribute] Attribute `obj` is not defined on `_T@walk & _dummy`, `Iterable[_T@walk | Iterable[_T@walk]] & _dummy` in union `(_T@walk & Unknown & ~None) | (Iterable[_T@walk | Iterable[_T@walk]] & Unknown) | (_T@walk & _dummy) | (Iterable[_T@walk | Iterable[_T@walk]] & _dummy)`

pytest (https://github.com/pytest-dev/pytest)
- src/_pytest/assertion/rewrite.py:1012:25 error[invalid-assignment] Invalid subscript assignment with key of type `@Todo` and value of type `expr` on object of type `dict[str, str]`
+ src/_pytest/assertion/rewrite.py:1012:25 error[invalid-assignment] Invalid subscript assignment with key of type `Unknown` and value of type `expr` on object of type `dict[str, str]`
- src/_pytest/assertion/rewrite.py:1110:17 error[invalid-assignment] Invalid subscript assignment with key of type `@Todo` and value of type `NamedExpr` on object of type `dict[str, str]`
+ src/_pytest/assertion/rewrite.py:1110:17 error[invalid-assignment] Invalid subscript assignment with key of type `Unknown` and value of type `NamedExpr` on object of type `dict[str, str]`
- src/_pytest/assertion/rewrite.py:1128:21 error[invalid-assignment] Invalid subscript assignment with key of type `@Todo` and value of type `NamedExpr` on object of type `dict[str, str]`
+ src/_pytest/assertion/rewrite.py:1128:21 error[invalid-assignment] Invalid subscript assignment with key of type `Unknown` and value of type `NamedExpr` on object of type `dict[str, str]`

schema_salad (https://github.com/common-workflow-language/schema_salad)
+ src/schema_salad/dotnet_codegen.py:509:45 error[invalid-argument-type] Argument to function `DotNetCodeGen.safe_name` is incorrect: Expected `str`, found `(dict[str, Any] & ~Top[MutableSequence[Unknown]]) | (str & ~Top[MutableSequence[Unknown]])`
+ src/schema_salad/java_codegen.py:571:45 error[invalid-argument-type] Argument to function `JavaCodeGen.safe_name` is incorrect: Expected `str`, found `(dict[str, Any] & ~Top[MutableSequence[Unknown]]) | (str & ~Top[MutableSequence[Unknown]])`
+ src/schema_salad/python_codegen.py:490:40 error[invalid-argument-type] Argument to function `PythonCodeGen.safe_name` is incorrect: Expected `str`, found `(dict[str, Any] & ~Top[MutableSequence[Unknown]]) | (str & ~Top[MutableSequence[Unknown]])`
+ src/schema_salad/typescript_codegen.py:444:40 error[invalid-argument-type] Argument to function `TypeScriptCodeGen.safe_name` is incorrect: Expected `str`, found `(dict[str, Any] & ~Top[MutableSequence[Unknown]]) | (str & ~Top[MutableSequence[Unknown]])`

Full report with detailed diff (timing results)

@charliermarsh charliermarsh force-pushed the charlie/revive-equality-narrowing branch from a923ba3 to d3b7208 Compare June 13, 2026 12:52
@charliermarsh charliermarsh force-pushed the charlie/revive-equality-narrowing branch from d3b7208 to 996f4e8 Compare June 13, 2026 13:33
@charliermarsh charliermarsh force-pushed the charlie/pattern-as branch 3 times, most recently from cd30fbb to 5919bfb Compare June 13, 2026 15:40
@charliermarsh charliermarsh changed the base branch from charlie/revive-equality-narrowing to charlie/reuse-equality-compatibility June 13, 2026 15:40
@charliermarsh charliermarsh force-pushed the charlie/pattern-as branch 2 times, most recently from c35e207 to 0636474 Compare June 13, 2026 19:25
@charliermarsh charliermarsh force-pushed the charlie/reuse-equality-compatibility branch from 5ac9446 to e9ca170 Compare June 13, 2026 20:10
@charliermarsh charliermarsh force-pushed the charlie/reuse-equality-compatibility branch 2 times, most recently from 6621128 to 3c57454 Compare June 13, 2026 21:31
@charliermarsh charliermarsh force-pushed the charlie/pattern-as branch 2 times, most recently from 2a91303 to 279abdd Compare June 13, 2026 22:33
@charliermarsh charliermarsh changed the base branch from charlie/reuse-equality-compatibility to charlie/strict-membership-narrowing June 13, 2026 22:34
@charliermarsh charliermarsh force-pushed the charlie/strict-membership-narrowing branch from 2d8ff2b to bc33265 Compare June 14, 2026 02:17
@charliermarsh charliermarsh force-pushed the charlie/pattern-as branch 2 times, most recently from f81d5bb to a1661c5 Compare June 14, 2026 03:10
@charliermarsh charliermarsh force-pushed the charlie/pattern-as branch 2 times, most recently from fb131bf to 7340670 Compare June 14, 2026 03:53
@charliermarsh charliermarsh force-pushed the charlie/strict-membership-narrowing branch from e3b322e to 02a1ad2 Compare June 14, 2026 04:17
@charliermarsh charliermarsh changed the base branch from charlie/reuse-equality-compatibility to main June 20, 2026 19:18
@charliermarsh

Copy link
Copy Markdown
Member Author

This PR covers sequences; #25941 extends the same behavior to class attributes and mappings.

@dhruvmanila dhruvmanila left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall, this change looks good to me. It’s a bit late here, and I’d like to spend more time on a few implementation details, but I don’t think that should block merging. Most of my comments are about expanding the test suite, they may uncover edge cases, but I don’t currently have any concerns to avoid merging this PR.

Comment thread crates/ty_ide/src/inlay_hints.rs
Comment thread crates/ty_python_semantic/resources/mdtest/conditional/match.md Outdated
Comment thread crates/ty_python_semantic/resources/mdtest/narrow/match.md Outdated
Comment thread crates/ty_python_semantic/resources/mdtest/narrow/match.md
Comment thread crates/ty_python_semantic/resources/mdtest/narrow/match.md
def test_incompatible_declared_capture(subject: int) -> None:
item: str
match subject:
case item: # error: [invalid-assignment]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Unrelated, I like the diagnostic message of mypy in this specific case considering the surrounding context of a pattern capture. This is just something I wanted to highlight and doesn't need to be implemented.

error: Incompatible types in capture pattern (pattern captures type "int", variable has type "str")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a reason that most of these test cases have single case arms? Could we have either an additional case arm, which could just be a catch-all, testing (reveal_type) the other side of the narrowing? Or, do you think it's not worth it?

Comment thread crates/ty_python_semantic/resources/mdtest/narrow/match.md
Comment on lines +605 to +615
def test_constructed_mutable_sequence_alias_does_not_keep_length(
value: list[int],
) -> int: # error: [invalid-return-type]
match value.copy():
case [_] as whole:
whole.append(2)
match whole:
case [_]:
return 1
case _:
raise ValueError

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure what this test case is testing? The invalid-return-type is coming from the inner match not matching the case expression. Removing the whole.append leads to no change.

Same question for the next test case which is similar but uses nested sequence pattern.

Comment thread crates/ty_python_semantic/resources/mdtest/narrow/match.md Outdated
@dhruvmanila

Copy link
Copy Markdown
Member

Actually, Codex found a couple of issues:

I think (1) is interesting, (2) will require __match_args__ which I don't think is supported currently (astral-sh/ty#887) and (3) is related to one of the comment that I raised.


  1. [P1] Later cases can narrow a capture instead of the saved match subject.
    In builder.rs, original subject bindings are preserved only when the current case rebinds the subject. An earlier guarded case can rebind it and then fail its guard:

    x = (1,)
    match x:
        case [x] if flag:
            pass
        case [1]:
            reveal_type(x)  # ty: Never; runtime: Literal[1]

    The second predicate is attached to the capture from case one rather than the tuple binding used when the subject was evaluated. This also suppresses an invalid x + "bad" operation. The original subject binding IDs need to be retained for every case.

  2. [P1] Attribute-bearing class patterns are treated as definite matches.
    match_pattern.rs subtracts every instance matched by Class(_, Irrefutable). However, an irrefutable child capture does not make attribute extraction irrefutable:

    class C:
        __match_args__ = ("missing",)
    
    match C():
        case C(item):
            pass
        case remainder:
            reveal_type(remainder)  # ty: Never, but this branch runs

    Until attribute availability can be proven, argument-bearing class patterns cannot contribute the whole class to definite-match subtraction.

  3. [P1] Mutable aliases retain stale negative sequence-shape facts.
    narrow.rs removes positive length/index facts for mutable aliases, but subject_ty can already contain negative shape constraints from earlier cases:

    match value:
        case []:
            pass
        case whole:
            whole.clear()
            match whole:
                case []:
                    reveal_type(whole)  # ty: Never; runtime branch is reached

    Both positive and negative structural facts must be excluded from the durable type of a mutable alias.

@charliermarsh

Copy link
Copy Markdown
Member Author

(2) is solved by a subsequent PR, intentionally not included here.

@charliermarsh

Copy link
Copy Markdown
Member Author

(I will review the others; I thought I already fixed (1). Thanks!)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants