From 0c7d2b8794772b3e61c62181e555fba2e0b769b9 Mon Sep 17 00:00:00 2001 From: Majid Feiz Date: Tue, 16 Jun 2026 14:15:30 +0330 Subject: [PATCH 1/2] Fix type variable inference for T | Wrapper[T] union patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When inferring type arguments for a generic function with a parameter of the form `T | list[T]` (or any `T | Wrapper[T]` pattern), mypy incorrectly inferred `T = Never` instead of the element type. Root cause: `any_constraints()` received two competing options from the union branches — `T >= list[int]` from the bare `T` branch and `T = int` from the `list[T]` branch. Because these are structurally incompatible, `any_constraints()` gave up and returned `[]`, leaving T unconstrained and causing the solver to fall back to `Never`. `handle_recursive_union()` already resolves this correctly by trying parameterized items first (yielding precise bounds like `T = int`) and falling back to bare TypeVar items only when needed. It was gated behind `has_recursive_types()`, which returns `False` for ordinary generic function signatures. The fix extends the fallback condition to also trigger when the union contains a bare TypeVar alongside a parameterized item that itself contains TypeVars (e.g. `list[T]`). The guard deliberately excludes unions like `AnyStr | int` where the non-TypeVar branch carries no TypeVars, preventing regressions with constrained TypeVars. Fixes #21615 --- mypy/constraints.py | 23 +++++++++++++++++------ test-data/unit/check-inference.test | 22 +++++++++++++++------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 48cc23f742227..0423341715cd7 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -424,7 +424,16 @@ def _infer_constraints( ) if result: return result - elif has_recursive_types(template) and not has_recursive_types(actual): + elif ( + has_recursive_types(template) + or ( + any(isinstance(t, TypeVarType) for t in template.items) + and any( + not isinstance(t, TypeVarType) and has_type_vars(t) + for t in template.items + ) + ) + ) and not has_recursive_types(actual): return handle_recursive_union(template, actual, direction) return [] @@ -504,11 +513,13 @@ def merge_with_any(constraint: Constraint) -> Constraint: def handle_recursive_union(template: UnionType, actual: Type, direction: int) -> list[Constraint]: - # This is a hack to special-case things like Union[T, Inst[T]] in recursive types. Although - # it is quite arbitrary, it is a relatively common pattern, so we should handle it well. - # This function may be called when inferring against such union resulted in different - # constraints for each item. Normally we give up in such case, but here we instead split - # the union in two parts, and try inferring sequentially. + # Special-case unions of the form ``T | Wrapper[T]``, both in recursive type aliases and + # in plain generic function signatures (e.g. ``def f[T](x: T | list[T])``). + # When any_constraints() infers against each union branch independently it may yield + # conflicting results — e.g. ``T >= list[int]`` from the bare-T branch and ``T = int`` + # from the ``list[T]`` branch — and gives up, leaving T unconstrained. Instead, we + # split the union into non-TypeVar parts (tried first, since they give precise bounds + # like ``T = int``) and TypeVar parts (used as a fallback). non_type_var_items = [t for t in template.items if not isinstance(t, TypeVarType)] type_var_items = [t for t in template.items if isinstance(t, TypeVarType)] return infer_constraints( diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index a5b3ae7238a5a..47d6f9ad9ff20 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -873,13 +873,7 @@ def g(x: Union[T, List[T]]) -> List[T]: pass def h(x: List[str]) -> None: pass g('a')() # E: "list[str]" not callable -# The next line is a case where there are multiple ways to satisfy a constraint -# involving a Union. Either T = list[str] or T = str would turn out to be valid, -# but mypy doesn't know how to branch on these two options (and potentially have -# to backtrack later) and defaults to T = Never. The result is an -# awkward error message. Either a better error message, or simply accepting the -# call, would be preferable here. -g(['a']) # E: Argument 1 to "g" has incompatible type "list[str]"; expected "list[Never]" +g(['a']) h(g(['a'])) @@ -891,6 +885,20 @@ i(b, a, b) i(a, b, b) # E: Argument 1 to "i" has incompatible type "list[int]"; expected "list[str]" [builtins fixtures/list.pyi] +[case testUnionInferenceTypeVarWrapper] +# Regression test for https://github.com/python/mypy/issues/21615 +# T | Wrapper[T] should infer T from the wrapped branch when the argument matches it. +from typing import TypeVar, Union, List, Dict +T = TypeVar('T') +def f(x: Union[T, List[T]]) -> List[T]: pass +def g(x: Union[T, Dict[str, T]]) -> T: pass + +reveal_type(f(1)) # N: Revealed type is "builtins.list[builtins.int]" +reveal_type(f([1])) # N: Revealed type is "builtins.list[builtins.int]" +reveal_type(f(['a'])) # N: Revealed type is "builtins.list[builtins.str]" +reveal_type(g({'k': 1})) # N: Revealed type is "builtins.int" +[builtins fixtures/dict.pyi] + [case testCallableListJoinInference] from typing import Any, Callable From a96f8201cab863e598224d6358de4c7cf527457a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:54:56 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/constraints.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 0423341715cd7..1aa6b4200a85a 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -429,8 +429,7 @@ def _infer_constraints( or ( any(isinstance(t, TypeVarType) for t in template.items) and any( - not isinstance(t, TypeVarType) and has_type_vars(t) - for t in template.items + not isinstance(t, TypeVarType) and has_type_vars(t) for t in template.items ) ) ) and not has_recursive_types(actual):