Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -8643,12 +8643,13 @@ def flatten(t: Expression) -> list[Expression]:
def flatten_types(t: Type) -> list[Type]:
"""Flatten a nested sequence of tuples into one list of nodes."""
t = get_proper_type(t)
if isinstance(t, UnionType):
return [b for a in t.items for b in flatten_types(a)]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm this seems wrong? Now if you have t: tuple[type[int]] | tuple[type[str]] or something and you do if isinstance(x, t):, won't we incorrectly narrow t in the else branch?

Copy link
Collaborator Author

@hauntsaninja hauntsaninja Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! We won't, but it is possible we might do the incorrect narrowing you suggest for final types in the future. I will add some more test cases to #20675.

The right fix is probably elsewhere, in conditional_types. Over here we are returning a list of types, not a union, and we should distribute over proposed type ranges instead of union-ing here:

mypy/mypy/checker.py

Lines 8380 to 8388 in 8bf049f

proposed_precise_type = UnionType.make_union(
[type_range.item for type_range in proposed_type_ranges if not type_range.is_upper_bound]
)
remaining_type = restrict_subtype_away(
current_type,
proposed_precise_type,
consider_runtime_isinstance=consider_runtime_isinstance,
)
return proposed_type, remaining_type

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if isinstance(t, TupleType):
return [b for a in t.items for b in flatten_types(a)]
elif is_named_instance(t, "builtins.tuple"):
return [t.args[0]]
else:
return [t]
return [t]


def expand_func(defn: FuncItem, map: dict[TypeVarId, Type]) -> FuncItem:
Expand Down
23 changes: 23 additions & 0 deletions test-data/unit/check-isinstance.test
Original file line number Diff line number Diff line change
Expand Up @@ -2938,3 +2938,26 @@ def foo(x: object, t: type[Any]):
if isinstance(x, t):
reveal_type(x) # N: Revealed type is "Any"
[builtins fixtures/isinstance.pyi]

[case testIsInstanceUnionOfTuples]
# flags: --strict-equality --warn-unreachable
from __future__ import annotations
from typing import TypeVar, Iterator

T1 = TypeVar("T1")
T2 = TypeVar("T2")
T3 = TypeVar("T3")

def extract(
values: object,
ts: (
tuple[type[T1]]
| tuple[type[T1], type[T2]]
| tuple[type[T1], type[T2], type[T3]]
)
) -> Iterator[T1 | T2 | T3]:
if isinstance(values, ts):
reveal_type(values) # N: Revealed type is "T1`-1 | T2`-2 | T3`-3"
yield values
raise
[builtins fixtures/primitives.pyi]