Skip to content

fix: isinstance narrowing regression with dynamic tuple argument#21190

Open
Bahtya wants to merge 2 commits intopython:masterfrom
Bahtya:fix/isinstance-narrowing-union-classinfo
Open

fix: isinstance narrowing regression with dynamic tuple argument#21190
Bahtya wants to merge 2 commits intopython:masterfrom
Bahtya:fix/isinstance-narrowing-union-classinfo

Conversation

@Bahtya
Copy link
Copy Markdown

@Bahtya Bahtya commented Apr 8, 2026

Problem

When isinstance is called with a dynamically-computed tuple of types (e.g. isinstance(exc, tuple(expected_excs))), mypy 1.20 incorrectly narrows the expression to object instead of keeping its already-narrowed type. This is a regression from 1.19.

def f(exc: BaseException | None, expected: set[type[BaseException]]) -> None:
    if exc is not None:
        if isinstance(exc, tuple(expected)):
            reveal_type(exc)  # 1.19: BaseException ✅ | 1.20: object ❌

Root Cause

The second argument to isinstance has its type stored as the expanded _ClassInfo recursive type alias (type | types.UnionType | tuple[_ClassInfo, ...]) rather than the precise tuple[type[BaseException], ...].

In get_type_range_of_type, the UnionType handler decomposes this union into individual members. The bare type member produces TypeRange(object, is_upper_bound=True), which after union simplification becomes the sole result. This causes conditional_types to narrow to object — a broader type than the already-narrowed BaseException.

In mypy 1.19, the raw UnionType was not handled by the type-range logic (it fell through to return None), which correctly preserved the existing narrowed type.

Solution

In get_type_range_of_type UnionType handler:

  1. Propagate None if any sub-item returns None (uncertainty)
  2. Filter out UninhabitedType entries before simplifying
  3. If the simplified result is just object, return None — this indicates type precision was lost (e.g. from the _ClassInfo widening), and we should fall back to keeping the current type

Testing

  • Verified fix against the original issue reproduction
  • All 248 isinstance tests pass
  • All 209 narrowing tests pass
  • All 843 optional/tuple tests pass
  • No regressions observed

Fixes #21181

Bahtya and others added 2 commits April 9, 2026 06:51
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 python#21181

Signed-off-by: bahtya <bahtyar153@qq.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

Diff from mypy_primer, showing the effect of this PR on open source code:

psycopg (https://github.com/psycopg/psycopg)
- psycopg/psycopg/types/numeric.py:407: error: No overload variant of "int" matches argument type "object"  [call-overload]
- psycopg/psycopg/types/numeric.py:407: note: Possible overload variants:
- psycopg/psycopg/types/numeric.py:407: note:     def int(str | Buffer | SupportsInt | SupportsIndex = ..., /) -> int
- psycopg/psycopg/types/numeric.py:407: note:     def int(str | bytes | bytearray, /, base: SupportsIndex) -> int

hydra-zen (https://github.com/mit-ll-responsible-ai/hydra-zen)
+ src/hydra_zen/_launch.py:92: error: Redundant cast to "T"  [redundant-cast]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[1.20 regression] when same object is narrowed multiple times

1 participant