Skip to content

Fix generic class pattern match widening solved type args to bound (#11526)#11527

Merged
rchiodo merged 1 commit into
microsoft:mainfrom
rchiodo:rchiodo-fix-match-bounded-typevar-narrowing
Jun 25, 2026
Merged

Fix generic class pattern match widening solved type args to bound (#11526)#11527
rchiodo merged 1 commit into
microsoft:mainfrom
rchiodo:rchiodo-fix-match-bounded-typevar-narrowing

Conversation

@rchiodo

@rchiodo rchiodo commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #11526, a regression introduced in 1.1.411 by #11489 where a match against a generic class pattern loses an already-solved type argument and widens it to the type parameter's bound.

Problem

class Wrapper[A: str | list]:
    data: A

def failing_match_unpack(obj: Wrapper[list]) -> list:
    match obj:
        case Wrapper(data=data):
            return data  # error: "str | list[Unknown]" is not assignable to "list[Unknown]"

obj has a concrete type argument (list), but after the class pattern match data is narrowed to the full bound str | list instead of list.

Root cause

#11489 added specializeBoundedMatchTypeParams, which falls back to a bounded type parameter's bound when the parameter is left unsolved by the constraint solver. The guard that decides whether an argument was solved used:

if (specializedArg && !containsAnyOrUnknown(specializedArg, /* recurse */ true)) {
    return specializedArg;
}

With recurse: true, containsAnyOrUnknown descends into type arguments. So a concrete-but-implicitly-parameterized argument — bare list is list[Unknown], dict is dict[Unknown, Unknown], or explicit list[Any] — was treated as "unsolved" and replaced by the parameter's bound.

This matches every observation in the issue:

  • int (no params) and list[int] (no nested Unknown) → kept → works
  • bare list, dict, set, list[Any] → nested Unknown/Any found → widened to bound → fails

The genuine "unsolved" sentinel is a bare top-level Unknown (the case #11489 targeted, where the subject carries no type arguments). A solved list[Unknown] is a real list class and must be preserved.

Fix

Only treat a bare top-level Unknown as the unsolved sentinel:

if (specializedArg && !isUnknown(specializedArg)) {
    return specializedArg;
}

This matches the original intent ("keep an argument unless it is genuinely Unknown") while preserving concrete arguments.

Testing

  • Extended matchNamedTupleGeneric1.py with regression cases for Wrapper[list], Wrapper[list[Any]], Wrapper[list[int]], and MultiWrapper[dict]; the existing NamedTuple12 test continues to assert 0 errors.
  • All original Fix generic NamedTuple match narrowing for bounded type vars #11489 cases still pass (subject narrowed against object still falls back to the bound, in-scope TypeVar arguments preserved, generic-supertype case, etc.).
  • Full pyright-internal suite passes (the only failing suite is languageServer.test.ts, which requires the prebuilt test server bundle and is unrelated).

PR microsoft#11489 added a fallback that specializes unsolved bounded type

parameters to their bound after a class pattern match. The guard that

distinguishes a solved argument from an unsolved one used

containsAnyOrUnknown(arg, recurse=true), which descends into type

arguments. As a result a concrete-but-implicitly-parameterized argument

(bare list = list[Unknown], dict, set, list[Any]) was misclassified as

unsolved and widened to the parameter's bound, breaking previously valid

code (issue microsoft#11526).

Only treat a bare top-level Unknown - the constraint solver's unsolved

sentinel - as needing the bound fallback, so concrete arguments are

preserved.

Fixes microsoft#11526

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

sympy (https://github.com/sympy/sympy)
-   .../projects/sympy/sympy/solvers/tests/test_solveset.py:315:25 - error: "Set" is not iterable
-     "__iter__" method not defined (reportGeneralTypeIssues)
-   .../projects/sympy/sympy/solvers/tests/test_solveset.py:315:25 - error: "ConditionSet" is not iterable
-     "__iter__" method not defined (reportGeneralTypeIssues)
-   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2030:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
-   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2042:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
-   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2055:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
-   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2076:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
-     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
-       "Set" is incompatible with protocol "Sized"
-         "__len__" is not present (reportArgumentType)
-   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2077:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
-   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2270:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
-     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
-       "Set" is incompatible with protocol "Sized"
-         "__len__" is not present (reportArgumentType)
-   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2351:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
-     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
-       "Set" is incompatible with protocol "Sized"
-         "__len__" is not present (reportArgumentType)
-   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2364:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
-     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
-       "Set" is incompatible with protocol "Sized"
-         "__len__" is not present (reportArgumentType)
-   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2370:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
-     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
-       "Set" is incompatible with protocol "Sized"
-         "__len__" is not present (reportArgumentType)
-   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2538:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
-     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
-       "Set" is incompatible with protocol "Sized"
-         "__len__" is not present (reportArgumentType)
-   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2541:20 - error: Argument of type "Unknown | Basic | Any" cannot be assigned to parameter "obj" of type "Sized" in function "len"
-     Type "Unknown | Basic | Any" is not assignable to type "Sized"
-       "Basic" is incompatible with protocol "Sized"
-         "__len__" is not present (reportArgumentType)
-   .../projects/sympy/sympy/stats/crv_types.py:2544:9 - error: Method "_cdf" overrides class "SingleContinuousDistribution" in an incompatible manner
-     Return type mismatch: base method returns type "None", override returns type "ComplexInfinity | NaN | Rational | Unknown | Zero | Expr"
-       Type "ComplexInfinity | NaN | Rational | Unknown | Zero | Expr" is not assignable to type "None"
-         "Expr" is not assignable to "None" (reportIncompatibleMethodOverride)
-   .../projects/sympy/sympy/stats/crv_types.py:2723:9 - error: Method "_cdf" overrides class "SingleContinuousDistribution" in an incompatible manner
-     Return type mismatch: base method returns type "None", override returns type "Expr | NaN | ComplexInfinity | Rational | Unknown | One | NegativeOne | Zero | Integer"
-       Type "Expr | NaN | ComplexInfinity | Rational | Unknown | One | NegativeOne | Zero | Integer" is not assignable to type "None"
-         "Expr" is not assignable to "None" (reportIncompatibleMethodOverride)
-   .../projects/sympy/sympy/stats/drv.py:269:22 - error: Argument of type "Generator[tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None, None, None]" cannot be assigned to parameter "iterable" of type "Iterable[_SupportsSumNoDefaultT@sum]" in function "sum"
+   .../projects/sympy/sympy/stats/drv.py:269:22 - error: Argument of type "Generator[tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None, None, None]" cannot be assigned to parameter "iterable" of type "Iterable[_SupportsSumNoDefaultT@sum]" in function "sum"
-     "Generator[tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None, None, None]" is not assignable to "Iterable[_SupportsSumNoDefaultT@sum]"
+     "Generator[tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None, None, None]" is not assignable to "Iterable[_SupportsSumNoDefaultT@sum]"
-       Type parameter "_T_co@Iterable" is covariant, but "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None" is not a subtype of "_SupportsSumNoDefaultT@sum"
+       Type parameter "_T_co@Iterable" is covariant, but "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None" is not a subtype of "_SupportsSumNoDefaultT@sum"
-         Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None" is not assignable to type "_SupportsSumWithNoDefaultGiven"
+         Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None" is not assignable to type "_SupportsSumWithNoDefaultGiven"
-           Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None" is not assignable to type "_SupportsSumWithNoDefaultGiven"
+           Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None" is not assignable to type "_SupportsSumWithNoDefaultGiven"
-   .../projects/sympy/sympy/stats/drv_types.py:293:16 - error: Operator "*" not supported for types "Expr" and "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic"
+   .../projects/sympy/sympy/stats/drv_types.py:293:16 - error: Operator "*" not supported for types "Expr" and "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic"
-   .../projects/sympy/sympy/stats/frv_types.py:139:17 - error: Argument of type "NaN | ComplexInfinity | Rational | Unknown | Expr" cannot be assigned to parameter "value" of type "int" in function "__setitem__"
-     Type "NaN | ComplexInfinity | Rational | Unknown | Expr" is not assignable to type "int"
-       "Expr" is not assignable to "int" (reportArgumentType)
-   .../projects/sympy/sympy/stats/random_matrix_models.py:111:36 - error: Operator "*" not supported for types "Expr" and "tuple[Unknown, ...] | Unknown | Product | Basic | BooleanFalse | BooleanTrue | ComplexInfinity | Equality | Expr | Float | Infinity | Integer | NaN | NegativeInfinity | NegativeOne | Number | One | Rational | Zero"
+   .../projects/sympy/sympy/stats/random_matrix_models.py:111:36 - error: Operator "*" not supported for types "Expr" and "tuple[Unknown, ...] | Unknown | Product | Basic | BooleanFalse | BooleanTrue | Equality | Expr | NaN | One | Zero"
-   .../projects/sympy/sympy/stats/rv.py:473:25 - error: Argument of type "Expr | Lambda | Zero | One | Integral | Unknown | Probability | tuple[Unknown, ...] | Sum | ZeroMatrix | Add | NaN | Piecewise | Basic | NegativeOne | Integer | ComplexInfinity | Rational | Infinity | NegativeInfinity | Float | Number | int" cannot be assigned to parameter "args" of type "Expr | complex" in function "__new__"
-     Type "Expr | Lambda | Zero | One | Integral | Unknown | Probability | tuple[Unknown, ...] | Sum | ZeroMatrix | Add | NaN | Piecewise | Basic | NegativeOne | Integer | ComplexInfinity | Rational | Infinity | NegativeInfinity | Float | Number | int" is not assignable to type "Expr | complex"
+   .../projects/sympy/sympy/stats/rv.py:473:25 - error: Argument of type "Expr | Lambda | Zero | One | Integral | Unknown | NegativeOne | Integer | NaN | ComplexInfinity | Rational | Infinity | Number | NegativeInfinity | Probability | tuple[Unknown, ...] | Sum | ZeroMatrix | Piecewise | Basic | int" cannot be assigned to parameter "args" of type "Expr | complex" in function "__new__"

... (truncated 1093 lines) ...

@rchiodo rchiodo enabled auto-merge (squash) June 25, 2026 16:49
@rchiodo rchiodo merged commit e1f6805 into microsoft:main Jun 25, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Regression in 1.1.411: match loses some of the context

2 participants