Skip to content

Don't narrow unions to Never on isinstance with a TypeVar classinfo - #5261

Open
Divyansh151005 wants to merge 1 commit into
facebook:mainfrom
Divyansh151005:fix/isinstance-typevar-classinfo-union
Open

Divyansh151005 wants to merge 1 commit into
facebook:mainfrom
Divyansh151005:fix/isinstance-typevar-classinfo-union

Conversation

@Divyansh151005

@Divyansh151005 Divyansh151005 commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #5147

isinstance with a type[T] classinfo narrowed a union-typed value to Never:

def fn[T](value: int | str, cls: type[T]):
    if isinstance(value, cls):
        reveal_type(value)  # before: Never, after: (int & T) | (str & T)

Narrowing to Never makes the rest of the branch look unreachable, so errors inside it are silently suppressed.

Root cause. When the narrowed value is a union, narrow_isinstance_from_definite intersects each member with the target using IntersectFallback::Never. That is intentional for concrete targets, so isinstance(x, Iterable) on int | list[int] narrows to just list[int]. A type variable target, however, is neither a subtype nor a supertype of any concrete member, so every member hit the fallback and the whole union became Never. (The non-union case already used the Right fallback and produced int & T.)

Fix. Use the regular Right fallback when the target is a type variable. Each member then intersects with T, producing (int & T) | (str & T). This is consistent with how we already narrow x: T against concrete classes (e.g. (float & T) | (int & T) in test_typevar_isinstance_narrow_else) and with what ty reports. Members that cannot overlap with the TypeVar's bound are still removed, since intersect_impl compares disjoint_bases and disjoint_base uses the bound: with T: int, int | str narrows to T.

The Never fallback for concrete targets is unchanged.

  • I am a human, and I fully understand the code I am submitting.

Test Plan

Added test_isinstance_union_with_typevar_classinfo to pyrefly/lib/test/narrow.rs, covering:

  • the issue's example: int | str narrowed by type[T] → (int & T) | (str & T); the negative branch stays int | str
  • a bounded TypeVar (T: int): int | str → T (the str member is dropped via the bound)
  • the non-union case: int → int & T (unchanged)
  • returning the narrowed value from a -> T function type-checks

Before the fix:

$ cargo test -p pyrefly --lib test_isinstance_union_with_typevar_classinfo
 INFO revealed type: Never [reveal-type]
4724 |         reveal_type(value)  # E: revealed type: (int & T) | (str & T)
test test::narrow::test_isinstance_union_with_typevar_classinfo ... FAILED

After:

$ cargo test -p pyrefly --lib test_isinstance_union_with_typevar_classinfo
test test::narrow::test_isinstance_union_with_typevar_classinfo ... ok

$ cargo test -p pyrefly --lib -- test::narrow:: test::pattern_match:: test::generic_restrictions:: test::type_guard test::typeis test::generic_basic:: test::simple:: test::type_var
test result: ok. 850 passed; 0 failed

$ cargo test -p pyrefly --lib -- --skip commands::coverage --skip lsp_interaction
test result: ok. 8762 passed; 0 failed

$ python3 test.py --no-test --no-tensor-shapes --no-conformance --no-jsonschema
Running formatting... Running linting... Running extension tests... OK

Fixes facebook#5147.

When the value being narrowed is a union, `narrow_isinstance_from_definite`
intersects each member with the isinstance target using a `Never` fallback,
so that e.g. `isinstance(x, Iterable)` on `int | list[int]` narrows to just
`list[int]`. With a `type[T]` classinfo every concrete member is unrelated
to `T`, so the whole union collapsed to `Never`.

Use the regular intersection fallback when the target is a type variable,
giving `(int & T) | (str & T)` (consistent with narrowing `x: T` against
concrete classes). Members that cannot overlap with the TypeVar's bound are
still dropped via `disjoint_base`.
@meta-codesync

meta-codesync Bot commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

This pull request has been imported. If you are a Meta employee, you can view this in D125878285. (Because this pull request was imported automatically, there will not be any future comments.)

@github-actions

Copy link
Copy Markdown

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

narrowing value with union type with isinstance where the class is generic incorrectly narrows to Never

2 participants