From ac2b245089e0a97be65a64aef9ead1dedfe3c69f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 13 Nov 2025 01:44:37 +0000 Subject: [PATCH] Fix crash on recursive tuple with Hashable --- mypy/typeops.py | 8 ++++---- mypy/types.py | 6 +++++- test-data/unit/check-python312.test | 12 ++++++++++++ test-data/unit/fixtures/tuple.pyi | 1 + 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/mypy/typeops.py b/mypy/typeops.py index d2f9f4da44e40..050252eb62050 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -666,15 +666,15 @@ def _remove_redundant_union_items(items: list[Type], keep_erased: bool) -> list[ else: # If not, check if we've seen a supertype of this type for j, tj in enumerate(new_items): - tj = get_proper_type(tj) + proper_tj = get_proper_type(tj) # If tj is an Instance with a last_known_value, do not remove proper_ti # (unless it's an instance with the same last_known_value) if ( - isinstance(tj, Instance) - and tj.last_known_value is not None + isinstance(proper_tj, Instance) + and proper_tj.last_known_value is not None and not ( isinstance(proper_ti, Instance) - and tj.last_known_value == proper_ti.last_known_value + and proper_tj.last_known_value == proper_ti.last_known_value ) ): continue diff --git a/mypy/types.py b/mypy/types.py index 056b99cc3f912..09e4b74bb821c 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -4159,7 +4159,11 @@ def flatten_nested_unions( tp = t if isinstance(tp, ProperType) and isinstance(tp, UnionType): flat_items.extend( - flatten_nested_unions(tp.items, handle_type_alias_type=handle_type_alias_type) + flatten_nested_unions( + tp.items, + handle_type_alias_type=handle_type_alias_type, + handle_recursive=handle_recursive, + ) ) else: # Must preserve original aliases when possible. diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index 840a708fecf33..c8b306b6bd551 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -2192,3 +2192,15 @@ y3: A3[int] # E: Type argument "int" of "A3" must be a subtype of "B3" z3: A3[None] [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] + +[case testPEP695TypeAliasRecursiveTupleUnionNoCrash] +from collections.abc import Hashable + +type HashableArg = int | tuple[Hashable | HashableArg] +x: HashableArg +reveal_type(x) # N: Revealed type is "Union[builtins.int, tuple[Union[typing.Hashable, ...]]]" +if isinstance(x, tuple): + y, = x + reveal_type(y) # N: Revealed type is "Union[typing.Hashable, Union[builtins.int, tuple[Union[typing.Hashable, ...]]]]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] diff --git a/test-data/unit/fixtures/tuple.pyi b/test-data/unit/fixtures/tuple.pyi index d01cd0034d263..06dfcf5d0fbc1 100644 --- a/test-data/unit/fixtures/tuple.pyi +++ b/test-data/unit/fixtures/tuple.pyi @@ -14,6 +14,7 @@ class type: def __init__(self, *a: object) -> None: pass def __call__(self, *a: object) -> object: pass class tuple(Sequence[_Tco], Generic[_Tco]): + def __hash__(self) -> int: ... def __new__(cls: Type[_T], iterable: Iterable[_Tco] = ...) -> _T: ... def __iter__(self) -> Iterator[_Tco]: pass def __contains__(self, item: object) -> bool: pass