Skip to content

Fix hang in constraint solver for cyclic TypeVar lower bounds (#11413)#11415

Merged
rchiodo merged 2 commits into
microsoft:mainfrom
rchiodo:rchiodo/wraptHang
May 4, 2026
Merged

Fix hang in constraint solver for cyclic TypeVar lower bounds (#11413)#11415
rchiodo merged 2 commits into
microsoft:mainfrom
rchiodo:rchiodo/wraptHang

Conversation

@rchiodo
Copy link
Copy Markdown
Collaborator

@rchiodo rchiodo commented May 4, 2026

Summary

Fixes #11413.

Pyright would hang indefinitely while analyzing code that combined wrapt 2.1.x's wrap_function_wrapper with a generic make_wrapper factory whose return type was a union containing both R and Awaitable[R]. The user-reported repro is at https://github.com/Gatedd/pyright-minimal-repo.

Root cause

In assignUnconstrainedTypeVar (packages/pyright-internal/src/analyzer/constraintSolver.ts), when a TypeVar's first lower bound was being established, the source type was recorded unconditionally — even when that source type contained the destination TypeVar nested inside another generic constructor.

For the wrapt case the constraint solver received:

R := R | Awaitable[R]

There is no finite type that satisfies this. Each subsequent round of solveAndApplyConstraints substituted R with its own ever-growing bound, producing types of the form:

R | Awaitable[R] | Awaitable[R | Awaitable[R]] | Awaitable[R | Awaitable[R] | Awaitable[R | Awaitable[R]]] | ...

The recursion-depth cap (20) was hit per call, but the per-call combinatorial work grew exponentially, producing a hang rather than a clean infinite loop.

Fix

Add a classical occurs check before recording the lower bound. If adjSrcType references destType at a strictly nested position (e.g. R := F[R] or R := R | Awaitable[R]), refuse the assignment and return false.

The check is carefully scoped to allow two benign shapes that the existing logic already handles correctly:

  1. Bare top-level identity (T := T) — fine, it's the identity constraint.
  2. Top-level union member equal to the TypeVar (T := T | int, which arises naturally from protocol matching against a method returning T | int) — also fine; the existing solver resolves it. This is exercised by the existing Protocol44 test.

Only strictly nested references trigger the failure path.

Test

Adds paramSpec56 regression sample + test. Without the fix the test hangs the jest worker (SIGTERM after timeout); with the fix analysis completes in ~640ms.

Verification

  • All 1,233 typeEvaluator* and checker tests pass locally.
  • Existing related tests verified unaffected: Tuple19 (similar cyclic shape, surfaces an error via Eric's earlier tuple-depth cap), Protocol44 (legitimate T | int shape), all 56 other ParamSpec* tests.

Caveat

For the wrapt case the constraint solver detects the cycle and refuses to bind the TypeVar, but the failure isn't surfaced as a user-visible diagnostic in this particular path — it occurs deep inside bidirectional inference for an argument expression where the diag isn't threaded through. The user's original code is type-incorrect, but pyright won't currently flag it; the important behavioral change here is that analysis completes instead of hanging. Threading a diagnostic through that call site would be a separate improvement.

rchiodo added 2 commits May 4, 2026 09:12
…oft#11413)

When the constraint solver established a TypeVar's first lower bound, it
would unconditionally record the source type even when that source type
contained the destination TypeVar nested inside another generic
constructor (e.g. R := R | Awaitable[R], as produced when matching wrapt
2.1.x's wrap_function_wrapper against a generic make_wrapper). Each
subsequent round of solveAndApplyConstraints substituted R with its own
ever-growing bound, producing exponentially larger types and hanging the
analyzer.

Add an occurs check in assignUnconstrainedTypeVar: if adjSrcType
references destType at a strictly nested position, refuse the assignment
rather than recording a non-finitely-solvable constraint. Top-level
union members that are exactly the TypeVar (e.g. T := T | int from
protocol matching against T | int) are explicitly allowed and resolved
by the existing logic.

Adds a regression test (paramSpec56) that hangs without the fix and
completes in ~640ms with it.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

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

sympy (https://github.com/sympy/sympy)
+   .../projects/sympy/sympy/integrals/integrals.py:107:41 - error: Argument of type "list[tuple[_T_co@tuple, ...]]" cannot be assigned to parameter "iterable" of type "Iterable[_T_co@tuple]" in function "__new__" (reportArgumentType)
-     "FunctionType" is not assignable to "PlotInterval" (reportArgumentType)
+     Type "(self: Self@PlotInterval) -> (Any | Unknown)" is not assignable to type "Self@PlotInterval" (reportArgumentType)
-     "FunctionType" is not assignable to "PlotInterval" (reportArgumentType)
+     Type "(self: Self@PlotInterval) -> None" is not assignable to type "Self@PlotInterval" (reportArgumentType)
-     "FunctionType" is not assignable to "PlotInterval" (reportArgumentType)
+     Type "(self: Self@PlotInterval) -> Generator[Any | Unknown, Any, None]" is not assignable to type "Self@PlotInterval" (reportArgumentType)
-     "FunctionType" is not assignable to "PlotInterval" (reportArgumentType)
+     Type "(self: Self@PlotInterval) -> Generator[tuple[Any | Unknown, Any | Unknown], Any, None]" is not assignable to type "Self@PlotInterval" (reportArgumentType)
-     "FunctionType" is not assignable to "PlotModeBase" (reportArgumentType)
+     Type "(self: Self@PlotModeBase, function: Unknown) -> None" is not assignable to type "Self@PlotModeBase" (reportArgumentType)
-     "FunctionType" is not assignable to "PlotModeBase" (reportArgumentType)
+     Type "(self: Self@PlotModeBase, function: Unknown) -> None" is not assignable to type "Self@PlotModeBase" (reportArgumentType)
-     "FunctionType" is not assignable to "PlotModeBase" (reportArgumentType)
+     Type "(self: Self@PlotModeBase) -> None" is not assignable to type "Self@PlotModeBase" (reportArgumentType)
-     "FunctionType" is not assignable to "PlotModeBase" (reportArgumentType)
+     Type "(self: Self@PlotModeBase, v: Unknown) -> None" is not assignable to type "Self@PlotModeBase" (reportArgumentType)
-     "FunctionType" is not assignable to "PlotModeBase" (reportArgumentType)
+     Type "(self: Self@PlotModeBase, v: Unknown) -> None" is not assignable to type "Self@PlotModeBase" (reportArgumentType)
+   .../projects/sympy/sympy/polys/rings.py:852:34 - error: Type "PolyElement[Er@PolyElement]" cannot be assigned to type variable "Er@PolyElement"
+     Type "PolyElement[Er@PolyElement]" is not assignable to upper bound "RingElement" for type variable "Er@PolyElement"
+       "PolyElement[Er@PolyElement]" is incompatible with protocol "RingElement"
+         "__add__" is an incompatible type
+           Type "(other: PolyElement[Er@PolyElement] | Er@PolyElement | int | PolyElement[PolyElement[Er@PolyElement]], /) -> (PolyElement[Er@PolyElement] | PolyElement[PolyElement[Er@PolyElement]])" is not assignable to type "(other: PolyElement[Er@PolyElement] | int, /) -> PolyElement[Er@PolyElement]"
+             Function return type "PolyElement[Er@PolyElement] | PolyElement[PolyElement[Er@PolyElement]]" is incompatible with type "PolyElement[Er@PolyElement]" (reportInvalidTypeArguments)
+   .../projects/sympy/sympy/polys/rings.py:853:22 - error: Type "PolyElement[Er@PolyElement]" cannot be assigned to type variable "Er@PolyElement"
+     Type "PolyElement[Er@PolyElement]" is not assignable to upper bound "RingElement" for type variable "Er@PolyElement"
+       "PolyElement[Er@PolyElement]" is incompatible with protocol "RingElement"
+         "__add__" is an incompatible type
+           Type "(other: PolyElement[Er@PolyElement] | Er@PolyElement | int | PolyElement[PolyElement[Er@PolyElement]], /) -> (PolyElement[Er@PolyElement] | PolyElement[PolyElement[Er@PolyElement]])" is not assignable to type "(other: PolyElement[Er@PolyElement] | int, /) -> PolyElement[Er@PolyElement]"
+             Function return type "PolyElement[Er@PolyElement] | PolyElement[PolyElement[Er@PolyElement]]" is incompatible with type "PolyElement[Er@PolyElement]" (reportInvalidTypeArguments)
-   .../projects/sympy/sympy/solvers/tests/test_solvers.py:1214:18 - error: Operator "+" not supported for types "Basic" and "Expr" (reportOperatorIssue)
-   .../projects/sympy/sympy/solvers/tests/test_solveset.py:445:30 - error: Cannot access attribute "limit_denominator" for class "NaN"
-     Attribute "limit_denominator" is unknown (reportAttributeAccessIssue)
-   .../projects/sympy/sympy/solvers/tests/test_solveset.py:445:30 - error: Cannot access attribute "limit_denominator" for class "ComplexInfinity"
-     Attribute "limit_denominator" is unknown (reportAttributeAccessIssue)
+   .../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:2024:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2036:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2049:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2070: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:2071:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2252: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:2333: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:2346: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:2352: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:2519: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:2522: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)

... (truncated 773 lines) ...

prefect (https://github.com/PrefectHQ/prefect)
+ .../projects/prefect/src/prefect/_internal/concurrency/api.py
+   .../projects/prefect/src/prefect/_internal/concurrency/api.py:149:15 - error: Method "wait_for_call_in_new_thread" overrides class "_base" in an incompatible manner
+     Parameter 1 type mismatch: base parameter is type "_SyncOrAsyncCallable[(), T@wait_for_call_in_new_thread] | Call[T@wait_for_call_in_new_thread]", override parameter is type "(() -> T@wait_for_call_in_new_thread) | Call[T@wait_for_call_in_new_thread]"
+     Return type mismatch: base method returns type "T@wait_for_call_in_new_thread", override returns type "CoroutineType[Any, Any, T@wait_for_call_in_new_thread]"
+       Type "_SyncOrAsyncCallable[(), T@wait_for_call_in_new_thread] | Call[T@wait_for_call_in_new_thread]" is not assignable to type "(() -> T@wait_for_call_in_new_thread) | Call[T@wait_for_call_in_new_thread]"
+         Type "_SyncOrAsyncCallable[(), T@wait_for_call_in_new_thread]" is not assignable to type "(() -> T@wait_for_call_in_new_thread) | Call[T@wait_for_call_in_new_thread]"
+           "FunctionType" is not assignable to "Call[T@wait_for_call_in_new_thread]"
+           Type "_SyncOrAsyncCallable[(), T@wait_for_call_in_new_thread]" is not assignable to type "() -> T@wait_for_call_in_new_thread"
+             Function return type "T@wait_for_call_in_new_thread | Awaitable[T@wait_for_call_in_new_thread]" is incompatible with type "T@wait_for_call_in_new_thread"
+       Type "CoroutineType[Any, Any, T@wait_for_call_in_new_thread]" is not assignable to type "T@wait_for_call_in_new_thread" (reportIncompatibleMethodOverride)
+   .../projects/prefect/src/prefect/_internal/concurrency/api.py:163:9 - error: Method "call_in_new_thread" overrides class "_base" in an incompatible manner
+     Return type mismatch: base method returns type "T@call_in_new_thread", override returns type "Awaitable[T@call_in_new_thread]"
+       Type "Awaitable[T@call_in_new_thread]" is not assignable to type "T@call_in_new_thread" (reportIncompatibleMethodOverride)
+   .../projects/prefect/src/prefect/_internal/concurrency/api.py:170:9 - error: Method "call_in_loop_thread" overrides class "_base" in an incompatible manner
+     Return type mismatch: base method returns type "T@call_in_loop_thread", override returns type "Awaitable[T@call_in_loop_thread]"
+       Type "Awaitable[T@call_in_loop_thread]" is not assignable to type "T@call_in_loop_thread" (reportIncompatibleMethodOverride)
+   .../projects/prefect/src/prefect/_internal/concurrency/api.py:201:9 - error: Method "wait_for_call_in_new_thread" overrides class "_base" in an incompatible manner
+     Parameter 1 type mismatch: base parameter is type "_SyncOrAsyncCallable[(), T@wait_for_call_in_new_thread] | Call[T@wait_for_call_in_new_thread]", override parameter is type "(() -> T@wait_for_call_in_new_thread) | Call[T@wait_for_call_in_new_thread]"
+       Type "_SyncOrAsyncCallable[(), T@wait_for_call_in_new_thread] | Call[T@wait_for_call_in_new_thread]" is not assignable to type "(() -> T@wait_for_call_in_new_thread) | Call[T@wait_for_call_in_new_thread]"
+         Type "_SyncOrAsyncCallable[(), T@wait_for_call_in_new_thread]" is not assignable to type "(() -> T@wait_for_call_in_new_thread) | Call[T@wait_for_call_in_new_thread]"
+           "FunctionType" is not assignable to "Call[T@wait_for_call_in_new_thread]"
+           Type "_SyncOrAsyncCallable[(), T@wait_for_call_in_new_thread]" is not assignable to type "() -> T@wait_for_call_in_new_thread"
+             Function return type "T@wait_for_call_in_new_thread | Awaitable[T@wait_for_call_in_new_thread]" is incompatible with type "T@wait_for_call_in_new_thread" (reportIncompatibleMethodOverride)
+   .../projects/prefect/src/prefect/_internal/concurrency/api.py:223:9 - error: Method "call_in_loop_thread" overrides class "_base" in an incompatible manner
+     Return type mismatch: base method returns type "T@call_in_loop_thread", override returns type "Awaitable[T@call_in_loop_thread] | T@call_in_loop_thread"
+       Type "Awaitable[T@call_in_loop_thread] | T@call_in_loop_thread" is not assignable to type "T@call_in_loop_thread" (reportIncompatibleMethodOverride)
- 6223 errors, 184 warnings, 0 informations
+ 6228 errors, 184 warnings, 0 informations

core (https://github.com/home-assistant/core)
+     Type "(self: Self@HomeAssistant, hassjob: HassJob[..., Coroutine[Any, Any, _R@async_add_hass_job] | _R@async_add_hass_job], *args: Any, eager_start: bool = False, background: bool = False) -> (Future[_R@async_add_hass_job] | None)" is not assignable to type "(self: Self@HomeAssistant, hassjob: HassJob[..., Coroutine[Any, Any, _R@async_add_hass_job]], *args: Any, eager_start: bool = False, background: bool = False) -> (Future[_R@async_add_hass_job] | None)"
+       Parameter 2: type "HassJob[..., Coroutine[Any, Any, _R@async_add_hass_job]]" is incompatible with type "HassJob[..., Coroutine[Any, Any, _R@async_add_hass_job] | _R@async_add_hass_job]"
+         "HassJob[..., Coroutine[Any, Any, _R@async_add_hass_job]]" is not assignable to "HassJob[..., Coroutine[Any, Any, _R@async_add_hass_job] | _R@async_add_hass_job]"
+           Type parameter "_R_co@HassJob" is invariant, but "Coroutine[Any, Any, _R@async_add_hass_job]" is not the same as "Coroutine[Any, Any, _R@async_add_hass_job] | _R@async_add_hass_job" (reportInconsistentOverload)
-     Function return type "Future[Coroutine[Any, Any, _R@async_add_hass_job]] | None" is incompatible with type "Future[Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, _R@async_add_hass_job]]]]] | None"
-       Type "Future[Coroutine[Any, Any, _R@async_add_hass_job]] | None" is not assignable to type "Future[Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, _R@async_add_hass_job]]]]] | None"
-         Type "Future[Coroutine[Any, Any, _R@async_add_hass_job]]" is not assignable to type "Future[Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, _R@async_add_hass_job]]]]] | None"
-           "Future[Coroutine[Any, Any, _R@async_add_hass_job]]" is not assignable to "Future[Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, _R@async_add_hass_job]]]]]"
-             Type parameter "_T@Future" is invariant, but "Coroutine[Any, Any, _R@async_add_hass_job]" is not the same as "Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, _R@async_add_hass_job]]]]"
-           "Future[Coroutine[Any, Any, _R@async_add_hass_job]]" is not assignable to "None" (reportInconsistentOverload)
+     Type "(self: Self@HomeAssistant, hassjob: HassJob[..., Coroutine[Any, Any, _R@_async_add_hass_job] | _R@_async_add_hass_job], *args: Any, background: bool = False) -> (Future[_R@_async_add_hass_job] | None)" is not assignable to type "(self: Self@HomeAssistant, hassjob: HassJob[..., Coroutine[Any, Any, _R@_async_add_hass_job]], *args: Any, background: bool = False) -> (Future[_R@_async_add_hass_job] | None)"
+       Parameter 2: type "HassJob[..., Coroutine[Any, Any, _R@_async_add_hass_job]]" is incompatible with type "HassJob[..., Coroutine[Any, Any, _R@_async_add_hass_job] | _R@_async_add_hass_job]"
+         "HassJob[..., Coroutine[Any, Any, _R@_async_add_hass_job]]" is not assignable to "HassJob[..., Coroutine[Any, Any, _R@_async_add_hass_job] | _R@_async_add_hass_job]"
+           Type parameter "_R_co@HassJob" is invariant, but "Coroutine[Any, Any, _R@_async_add_hass_job]" is not the same as "Coroutine[Any, Any, _R@_async_add_hass_job] | _R@_async_add_hass_job" (reportInconsistentOverload)
-     Function return type "Future[Coroutine[Any, Any, _R@_async_add_hass_job]] | None" is incompatible with type "Future[Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, _R@_async_add_hass_job]]]]] | None"
-       Type "Future[Coroutine[Any, Any, _R@_async_add_hass_job]] | None" is not assignable to type "Future[Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, _R@_async_add_hass_job]]]]] | None"
-         Type "Future[Coroutine[Any, Any, _R@_async_add_hass_job]]" is not assignable to type "Future[Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, _R@_async_add_hass_job]]]]] | None"
-           "Future[Coroutine[Any, Any, _R@_async_add_hass_job]]" is not assignable to "Future[Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, _R@_async_add_hass_job]]]]]"
-             Type parameter "_T@Future" is invariant, but "Coroutine[Any, Any, _R@_async_add_hass_job]" is not the same as "Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, _R@_async_add_hass_job]]]]"
-           "Future[Coroutine[Any, Any, _R@_async_add_hass_job]]" is not assignable to "None" (reportInconsistentOverload)
+     Type "(self: Self@HomeAssistant, hassjob: HassJob[..., Coroutine[Any, Any, _R@async_run_hass_job] | _R@async_run_hass_job], *args: Any, background: bool = False) -> (Future[_R@async_run_hass_job] | None)" is not assignable to type "(self: Self@HomeAssistant, hassjob: HassJob[..., Coroutine[Any, Any, _R@async_run_hass_job]], *args: Any, background: bool = False) -> (Future[_R@async_run_hass_job] | None)"
+       Parameter 2: type "HassJob[..., Coroutine[Any, Any, _R@async_run_hass_job]]" is incompatible with type "HassJob[..., Coroutine[Any, Any, _R@async_run_hass_job] | _R@async_run_hass_job]"
+         "HassJob[..., Coroutine[Any, Any, _R@async_run_hass_job]]" is not assignable to "HassJob[..., Coroutine[Any, Any, _R@async_run_hass_job] | _R@async_run_hass_job]"
+           Type parameter "_R_co@HassJob" is invariant, but "Coroutine[Any, Any, _R@async_run_hass_job]" is not the same as "Coroutine[Any, Any, _R@async_run_hass_job] | _R@async_run_hass_job" (reportInconsistentOverload)
-     Function return type "Future[Coroutine[Any, Any, _R@async_run_hass_job]] | None" is incompatible with type "Future[Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, _R@async_run_hass_job]]]]] | None"
-       Type "Future[Coroutine[Any, Any, _R@async_run_hass_job]] | None" is not assignable to type "Future[Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, _R@async_run_hass_job]]]]] | None"
-         Type "Future[Coroutine[Any, Any, _R@async_run_hass_job]]" is not assignable to type "Future[Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, _R@async_run_hass_job]]]]] | None"
-           "Future[Coroutine[Any, Any, _R@async_run_hass_job]]" is not assignable to "Future[Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, _R@async_run_hass_job]]]]]"
-             Type parameter "_T@Future" is invariant, but "Coroutine[Any, Any, _R@async_run_hass_job]" is not the same as "Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, _R@async_run_hass_job]]]]"
-           "Future[Coroutine[Any, Any, _R@async_run_hass_job]]" is not assignable to "None" (reportInconsistentOverload)

xarray (https://github.com/pydata/xarray)
+   .../projects/xarray/xarray/backends/netCDF4_.py:439:13 - error: Argument of type "DummyFileManager[Unknown] | Unknown" cannot be assigned to parameter "manager" of type "Unknown" in function "__init__"
+     Type "DummyFileManager[Unknown] | Unknown" is not assignable to type "Unknown" (reportArgumentType)
- 2359 errors, 201 warnings, 0 informations
+ 2360 errors, 201 warnings, 0 informations

jax (https://github.com/google/jax)
+   .../projects/jax/jax/_src/checkify.py:133:7 - error: Method "tree_flatten" overrides class "JaxException" in an incompatible manner
+     Return type mismatch: base method returns type "tuple[list[Unknown], Unknown]", override returns type "tuple[list[Unknown], tuple[Unknown, Unknown]]"
+       "tuple[list[Unknown], tuple[Unknown, Unknown]]" is not assignable to "tuple[list[Unknown], Unknown]"
+         Tuple entry 2 is incorrect type
+           Type "tuple[Unknown, Unknown]" is not assignable to type "Unknown" (reportIncompatibleMethodOverride)
+   .../projects/jax/jax/_src/checkify.py:155:7 - error: Method "tree_flatten" overrides class "JaxException" in an incompatible manner
+     Return type mismatch: base method returns type "tuple[list[Unknown], Unknown]", override returns type "tuple[list[Unknown], tuple[Unknown, Unknown, Unknown]]"
+       "tuple[list[Unknown], tuple[Unknown, Unknown, Unknown]]" is not assignable to "tuple[list[Unknown], Unknown]"
+         Tuple entry 2 is incorrect type
+           Type "tuple[Unknown, Unknown, Unknown]" is not assignable to type "Unknown" (reportIncompatibleMethodOverride)
- 3288 errors, 92 warnings, 0 informations
+ 3290 errors, 92 warnings, 0 informations

@rchiodo
Copy link
Copy Markdown
Collaborator Author

rchiodo commented May 4, 2026

mypy_primer analysis

I went through every diff. Summary: all the changes are either correct improvements or genuine bugs that the cycle was previously masking. Net +8 errors across 5 projects, but those errors are real, and many of the "removed" errors were false positives. Several remaining errors also became dramatically more readable.

home-assistant/core — improvement (no count change)

All - lines have absurdly nested types like:

Future[Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, Coroutine[Any, Any, _R]]]]]

This is exactly the cyclic-explosion this PR is fixing — the constraint solver was repeatedly substituting _R := Coroutine[Any, Any, _R] | _R into itself. With the fix the diagnostics show the original Coroutine[Any, Any, _R] | _R cleanly.

sympy — mostly correct

Removed (false positives):

  • crv_types.py:2544 / :2723Method "_cdf" overrides ... returns type "ComplexInfinity | NaN | Rational | Unknown | Zero | Expr" not assignable to None. The base _cdf returns None; the overrides return Expr. The cyclic widening was previously fooling pyright into believing the base also returned Expr-like values, so the override "matched". Removing this error is correct (the real fix is unrelated — these overrides are arguably valid in sympy's design, but the error wasn't from this code path).
  • test_solvers.py:1214, test_solveset.py:445 (×2) — same family.

New (genuine):

  • polys/rings.py:852-853PolyElement[Er] doesn't satisfy RingElement protocol because its __add__ overload signature has a wider return type (PolyElement[Er] | PolyElement[PolyElement[Er]]) than RingElement.__add__ requires (PolyElement[Er]). I confirmed this in source: PolyElement.__add__ is genuinely overloaded with two return shapes, but the implementation signature widens. This is a real protocol mismatch.
  • integrals.py:107 — nested tuple([tuple(xab) for xab in self.limits]) — same _T_co flowing into itself; previously masked by cyclic widening.
  • Several test_solveset.py errors about Set / Basic not satisfying Sized / Iterable / __getitem__ — these are real protocol mismatches that the cycle was hiding.

Improved messages (no count change):

  • ~10 lines under plot/...: old message just said "FunctionType" is not assignable to "PlotInterval"; new message includes the actual function signature (Type "(self: Self@PlotInterval) -> ..."). More useful diagnostic.

prefect — genuine bugs uncovered (+5 errors)

All five new errors are in _internal/concurrency/api.py and have the shape:

override returns type "Awaitable[T]" / "CoroutineType[Any, Any, T]"
"Awaitable[T]" is not assignable to type "T"
Function return type "T | Awaitable[T]" is incompatible with type "T"

These are genuine override mismatches: the base class declares -> T but the subclass returns Awaitable[T]. Previously the cyclic constraint T := T | Awaitable[T] silently widened, making the subclass appear to return T. The fix correctly rejects the cycle, surfacing the real reportIncompatibleMethodOverride error.

jax — genuine bugs uncovered (+2 errors)

checkify.py:133 and :155tree_flatten overrides return tuple[list[Unknown], tuple[Unknown, Unknown]] while base returns tuple[list[Unknown], Unknown]. The cycle was masking a real override-type-mismatch on the second tuple element.

xarray — minor, arguably correct (+1 error)

netCDF4_.py:439NetCDF4DataStore.__init__ has unannotated manager. After an isinstance branch that reassigns to DummyFileManager, self._manager ends up typed DummyFileManager[Unknown] | Unknown. In get_child_store(self) -> Self, type(self)(self._manager, ...) now flags that the union argument can't satisfy the constructor's inferred parameter type. The error itself is awkwardly worded (anything should be assignable to Unknown), but the underlying issue is that the constructor is unannotated and the Unknown | Unknown union is being matched against itself with different scopes — same family of cycle. One spurious-looking error against the rest of the wins seems fine.

Bottom line

Project Error count change Verdict
home-assistant/core 0 improved messages
sympy mixed net improvement
prefect +5 real bugs uncovered
jax +2 real bugs uncovered
xarray +1 minor regression in error wording

I don't think any of these warrant a fix on this PR — the new errors are correct, the removed errors were false positives, and the improved messages are a nice bonus.

@rchiodo rchiodo merged commit f06cf49 into microsoft:main May 4, 2026
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.

Pyright hangs indefinitely when using wrapt > 2.0.0

2 participants