From 37d2ee49db02079e2862516d70d5e17c2fcbde89 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Thu, 2 Apr 2026 13:24:12 -0700 Subject: [PATCH 1/3] Fix regression involving dict or on recursive dict Fixes #21141 --- mypy/constraints.py | 8 +++++--- test-data/unit/check-recursive-types.test | 9 +++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index df79fdae5456c..ad2ce12baf464 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -506,9 +506,11 @@ def handle_recursive_union(template: UnionType, actual: Type, direction: int) -> # the union in two parts, and try inferring sequentially. 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( - UnionType.make_union(non_type_var_items), actual, direction - ) or infer_constraints(UnionType.make_union(type_var_items), actual, direction) + ret = infer_constraints(UnionType.make_union(non_type_var_items), actual, direction) + if ret or any(mypy.subtypes.is_subtype(t, actual) for t in non_type_var_items): + return ret + ret = infer_constraints(UnionType.make_union(type_var_items), actual, direction) + return ret def any_constraints(options: list[list[Constraint] | None], *, eager: bool) -> list[Constraint]: diff --git a/test-data/unit/check-recursive-types.test b/test-data/unit/check-recursive-types.test index 10333d113a689..c1ede22ac1aa5 100644 --- a/test-data/unit/check-recursive-types.test +++ b/test-data/unit/check-recursive-types.test @@ -1040,3 +1040,12 @@ def f(obj: OneClass) -> None: else: reveal_type(obj) # N: Revealed type is "builtins.list[...]" [builtins fixtures/isinstancelist.pyi] + +[case testRecursiveDictOr] +from typing_extensions import TypeAlias +A: TypeAlias = "dict[str, A | int]" + +def f(d1: A, d2: A) -> None: + d3: A = {**d1, **d2} + d4: A = d1 | d2 +[builtins fixtures/dict-full.pyi] From 7f48f4dfd3ac769a4533d46632dca2e57bdb419f Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Thu, 2 Apr 2026 14:11:02 -0700 Subject: [PATCH 2/3] fix fixture --- test-data/unit/fixtures/dict-full.pyi | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/test-data/unit/fixtures/dict-full.pyi b/test-data/unit/fixtures/dict-full.pyi index 4f4097b77788a..fef0d3c9624ad 100644 --- a/test-data/unit/fixtures/dict-full.pyi +++ b/test-data/unit/fixtures/dict-full.pyi @@ -8,6 +8,7 @@ from typing import ( ) T = TypeVar('T') +T1 = TypeVar('T1') T2 = TypeVar('T2') KT = TypeVar('KT') VT = TypeVar('VT') @@ -39,15 +40,8 @@ class dict(Mapping[KT, VT]): def get(self, key: KT, default: T, /) -> Union[VT, T]: pass def __len__(self) -> int: ... - # This was actually added in 3.9: - @overload - def __or__(self, __value: dict[KT, VT]) -> dict[KT, VT]: ... - @overload - def __or__(self, __value: dict[T, T2]) -> dict[Union[KT, T], Union[VT, T2]]: ... - @overload - def __ror__(self, __value: dict[KT, VT]) -> dict[KT, VT]: ... - @overload - def __ror__(self, __value: dict[T, T2]) -> dict[Union[KT, T], Union[VT, T2]]: ... + def __or__(self, value: dict[T1, T2], /) -> dict[KT | T1, VT | T2]: ... + def __ror__(self, value: dict[T1, T2], /) -> dict[KT | T1, VT | T2]: ... # dict.__ior__ should be kept roughly in line with MutableMapping.update() @overload # type: ignore[misc] def __ior__(self, __value: _typeshed.SupportsKeysAndGetItem[KT, VT]) -> Self: ... From 31a61a258221ffdbc918a8d8a144e707d599fa3c Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Thu, 2 Apr 2026 14:39:03 -0700 Subject: [PATCH 3/3] use is_same_type for symmetry --- mypy/constraints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index ad2ce12baf464..1176d144511cf 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -507,7 +507,7 @@ def handle_recursive_union(template: UnionType, actual: Type, direction: int) -> 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)] ret = infer_constraints(UnionType.make_union(non_type_var_items), actual, direction) - if ret or any(mypy.subtypes.is_subtype(t, actual) for t in non_type_var_items): + if ret or any(mypy.subtypes.is_same_type(t, actual) for t in non_type_var_items): return ret ret = infer_constraints(UnionType.make_union(type_var_items), actual, direction) return ret