From f65031509997d7c05802c5996d7ced7e341beec7 Mon Sep 17 00:00:00 2001 From: bahtya Date: Thu, 9 Apr 2026 06:51:18 +0800 Subject: [PATCH 1/5] fix: isinstance narrowing regression with dynamic tuple argument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When isinstance is called with a dynamically-computed tuple of types (e.g. isinstance(exc, tuple(expected_excs))), the second argument's stored type gets widened to match the _ClassInfo recursive type alias (type | types.UnionType | tuple[_ClassInfo, ...]). The union handler in get_type_range_of_type then decomposes this into individual members, producing TypeRange(object, ...) from bare 'type', which incorrectly narrows the expression to 'object' instead of keeping its existing type. Fix by checking if the simplified union result is just 'object' — which indicates we've lost type precision — and returning None to fall back to keeping the current type, matching the v1.19 behavior. Also handle None sub-items explicitly (propagate uncertainty) and filter out UninhabitedType entries before simplifying. Fixes #21181 Signed-off-by: bahtya --- mypy/checker.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 8775f1ddef29..175f8e182f12 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8028,7 +8028,17 @@ def get_type_range_of_type(self, typ: Type) -> TypeRange | None: if isinstance(typ, UnionType): type_ranges = [self.get_type_range_of_type(item) for item in typ.items] - item = make_simplified_union([t.item for t in type_ranges if t is not None]) + if any(t is None for t in type_ranges): + return None + valid_ranges = [t for t in type_ranges if not isinstance(get_proper_type(t.item), UninhabitedType)] + if not valid_ranges: + return TypeRange(UninhabitedType(), is_upper_bound=False) + # If the only meaningful type we can extract is "object", we've lost + # type precision (e.g. from a widened _ClassInfo alias). Return None + # to avoid narrowing to a useless type. + item = make_simplified_union([t.item for t in valid_ranges]) + if isinstance(get_proper_type(item), Instance) and get_proper_type(item).type.fullname == "builtins.object": + return None return TypeRange(item, is_upper_bound=True) if isinstance(typ, FunctionLike) and typ.is_type_obj(): # If a type is generic, `isinstance` can only narrow its variables to Any. From 1d491f6bc4d6a8017848e23d2cedbcd5a825b988 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:54:04 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 175f8e182f12..cb00b89b8c10 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8030,14 +8030,19 @@ def get_type_range_of_type(self, typ: Type) -> TypeRange | None: type_ranges = [self.get_type_range_of_type(item) for item in typ.items] if any(t is None for t in type_ranges): return None - valid_ranges = [t for t in type_ranges if not isinstance(get_proper_type(t.item), UninhabitedType)] + valid_ranges = [ + t for t in type_ranges if not isinstance(get_proper_type(t.item), UninhabitedType) + ] if not valid_ranges: return TypeRange(UninhabitedType(), is_upper_bound=False) # If the only meaningful type we can extract is "object", we've lost # type precision (e.g. from a widened _ClassInfo alias). Return None # to avoid narrowing to a useless type. item = make_simplified_union([t.item for t in valid_ranges]) - if isinstance(get_proper_type(item), Instance) and get_proper_type(item).type.fullname == "builtins.object": + if ( + isinstance(get_proper_type(item), Instance) + and get_proper_type(item).type.fullname == "builtins.object" + ): return None return TypeRange(item, is_upper_bound=True) if isinstance(typ, FunctionLike) and typ.is_type_obj(): From f1371ae3426de0ed0b09c76eb90cf401ee5ace9c Mon Sep 17 00:00:00 2001 From: Bahtya Date: Thu, 9 Apr 2026 19:27:24 +0800 Subject: [PATCH 3/5] fix: resolve type checking errors in get_type_range_of_type - Add explicit None check in valid_ranges comprehension to satisfy type checker - Store get_proper_type(item) result in variable to avoid calling it twice and enable proper type narrowing This fixes the 3 type checking errors introduced by the isinstance narrowing fix: - Item "None" of "TypeRange | None" has no attribute "item" - "ProperType" has no attribute "type" The logic remains the same, just restructured to pass strict type checking. --- mypy/checker.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index cb00b89b8c10..974530bf374d 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8030,8 +8030,10 @@ def get_type_range_of_type(self, typ: Type) -> TypeRange | None: type_ranges = [self.get_type_range_of_type(item) for item in typ.items] if any(t is None for t in type_ranges): return None + # Filter out None and UninhabitedType entries (type checkers need explicit None check) valid_ranges = [ - t for t in type_ranges if not isinstance(get_proper_type(t.item), UninhabitedType) + t for t in type_ranges + if t is not None and not isinstance(get_proper_type(t.item), UninhabitedType) ] if not valid_ranges: return TypeRange(UninhabitedType(), is_upper_bound=False) @@ -8039,10 +8041,8 @@ def get_type_range_of_type(self, typ: Type) -> TypeRange | None: # type precision (e.g. from a widened _ClassInfo alias). Return None # to avoid narrowing to a useless type. item = make_simplified_union([t.item for t in valid_ranges]) - if ( - isinstance(get_proper_type(item), Instance) - and get_proper_type(item).type.fullname == "builtins.object" - ): + proper_item = get_proper_type(item) + if isinstance(proper_item, Instance) and proper_item.type.fullname == "builtins.object": return None return TypeRange(item, is_upper_bound=True) if isinstance(typ, FunctionLike) and typ.is_type_obj(): From 94c063ca844b5f870df5174258100ebf5bc76c1e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:29:53 +0000 Subject: [PATCH 4/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 974530bf374d..b8436482128d 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8032,7 +8032,8 @@ def get_type_range_of_type(self, typ: Type) -> TypeRange | None: return None # Filter out None and UninhabitedType entries (type checkers need explicit None check) valid_ranges = [ - t for t in type_ranges + t + for t in type_ranges if t is not None and not isinstance(get_proper_type(t.item), UninhabitedType) ] if not valid_ranges: @@ -8042,7 +8043,10 @@ def get_type_range_of_type(self, typ: Type) -> TypeRange | None: # to avoid narrowing to a useless type. item = make_simplified_union([t.item for t in valid_ranges]) proper_item = get_proper_type(item) - if isinstance(proper_item, Instance) and proper_item.type.fullname == "builtins.object": + if ( + isinstance(proper_item, Instance) + and proper_item.type.fullname == "builtins.object" + ): return None return TypeRange(item, is_upper_bound=True) if isinstance(typ, FunctionLike) and typ.is_type_obj(): From f00e8f86ac8c6296dfc25573bd2402f10ef0f55d Mon Sep 17 00:00:00 2001 From: Bahtya Date: Thu, 9 Apr 2026 19:36:47 +0800 Subject: [PATCH 5/5] fix: remove redundant get_proper_type() call in checker.py The make_simplified_union return type is already specific enough for isinstance check, no need to unwrap further. Bahtya --- mypy/checker.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index b8436482128d..a38420ef97f3 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8042,11 +8042,7 @@ def get_type_range_of_type(self, typ: Type) -> TypeRange | None: # type precision (e.g. from a widened _ClassInfo alias). Return None # to avoid narrowing to a useless type. item = make_simplified_union([t.item for t in valid_ranges]) - proper_item = get_proper_type(item) - if ( - isinstance(proper_item, Instance) - and proper_item.type.fullname == "builtins.object" - ): + if isinstance(item, Instance) and item.type.fullname == "builtins.object": return None return TypeRange(item, is_upper_bound=True) if isinstance(typ, FunctionLike) and typ.is_type_obj():