From 683a924df37ee23258180145a487509b965e32ac Mon Sep 17 00:00:00 2001 From: Henry Su Date: Mon, 10 Aug 2026 00:16:34 -0500 Subject: [PATCH 1/2] Fix negative type narrowing for isinstance and issubclass when class filter is a type[T] variable or tuple of type[T] --- .../src/analyzer/typeGuards.ts | 15 +++++- .../samples/typeNarrowingIsinstance22.py | 47 +++++++++++++++++++ .../src/tests/typeEvaluator1.test.ts | 6 +++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance22.py diff --git a/packages/pyright-internal/src/analyzer/typeGuards.ts b/packages/pyright-internal/src/analyzer/typeGuards.ts index 7ee3a18d5b37..8ce4f456e2d1 100644 --- a/packages/pyright-internal/src/analyzer/typeGuards.ts +++ b/packages/pyright-internal/src/analyzer/typeGuards.ts @@ -1246,14 +1246,27 @@ export function getIsInstanceClassTypes( if (isInstantiableClass(subtype) && ClassType.isBuiltIn(subtype, 'Callable')) { subtype = convertToInstantiable(getUnknownTypeForCallable()); + } else if (TypeBase.isInstance(subtype)) { + if ( + ClassType.isBuiltIn(subtype, 'type') && + subtype.priv.typeArgs && + isAnyOrUnknown(subtype.priv.typeArgs[0]) + ) { + foundNonClassType = true; + return; + } + subtype = convertToInstantiable(subtype); } } if (isInstantiableClass(subtype)) { + if (subtype.priv.includeSubclasses) { + subtype = ClassType.cloneIncludeSubclasses(subtype, /* includeSubclasses */ false); + } // If this is a reference to a class that has type promotions (e.g. // float or complex), remove the promotions for purposes of the // isinstance check). - if (!subtype.priv.includeSubclasses && subtype.priv.includePromotions) { + if (subtype.priv.includePromotions) { subtype = ClassType.cloneRemoveTypePromotions(subtype); } classTypeList.push(subtype); diff --git a/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance22.py b/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance22.py new file mode 100644 index 000000000000..f90b182f8f6f --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance22.py @@ -0,0 +1,47 @@ +# This sample tests type narrowing for isinstance and issubclass when +# the class argument is passed as a type[T] variable or tuple of type[T]. + +# pyright: reportMissingModuleSource=false + +from typing_extensions import reveal_type + + +class A: + pass + + +class B: + pass + + +class C: + pass + + +def test_tuple_param(x: A | B | C, types: tuple[type[A], type[B]]): + if isinstance(x, types): + reveal_type(x, expected_text="A | B") + else: + reveal_type(x, expected_text="C") + + +def test_tuple_annotated_local(x: A | B | C): + types: tuple[type[A], type[B]] = (A, B) + if isinstance(x, types): + reveal_type(x, expected_text="A | B") + else: + reveal_type(x, expected_text="C") + + +def test_single_class_param(x: A | B, cls: type[A]): + if isinstance(x, cls): + reveal_type(x, expected_text="A") + else: + reveal_type(x, expected_text="B") + + +def test_issubclass_param(sub_cls: type[A] | type[B], cls: type[A]): + if issubclass(sub_cls, cls): + reveal_type(sub_cls, expected_text="type[A]") + else: + reveal_type(sub_cls, expected_text="type[B]") diff --git a/packages/pyright-internal/src/tests/typeEvaluator1.test.ts b/packages/pyright-internal/src/tests/typeEvaluator1.test.ts index 73306b3b11a4..e010e41267d8 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator1.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator1.test.ts @@ -507,6 +507,12 @@ test('TypeNarrowingIsinstance21', () => { TestUtils.validateResults(analysisResults, 0); }); +test('TypeNarrowingIsinstance22', () => { + const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingIsinstance22.py']); + + TestUtils.validateResults(analysisResults, 0); +}); + test('TypeNarrowingTupleLength1', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingTupleLength1.py']); From d023ce2f8729a53de7001fef9a677163d00de401 Mon Sep 17 00:00:00 2001 From: Henry Su Date: Mon, 10 Aug 2026 11:42:18 -0500 Subject: [PATCH 2/2] Fix type[T] filter extraction in getIsInstanceClassTypes and preserve soundness --- .../src/analyzer/typeGuards.ts | 34 +++++++++++-------- .../samples/typeNarrowingIsinstance22.py | 28 +++++---------- 2 files changed, 28 insertions(+), 34 deletions(-) diff --git a/packages/pyright-internal/src/analyzer/typeGuards.ts b/packages/pyright-internal/src/analyzer/typeGuards.ts index 8ce4f456e2d1..f6158a68646e 100644 --- a/packages/pyright-internal/src/analyzer/typeGuards.ts +++ b/packages/pyright-internal/src/analyzer/typeGuards.ts @@ -1246,27 +1246,27 @@ export function getIsInstanceClassTypes( if (isInstantiableClass(subtype) && ClassType.isBuiltIn(subtype, 'Callable')) { subtype = convertToInstantiable(getUnknownTypeForCallable()); - } else if (TypeBase.isInstance(subtype)) { - if ( - ClassType.isBuiltIn(subtype, 'type') && - subtype.priv.typeArgs && - isAnyOrUnknown(subtype.priv.typeArgs[0]) - ) { - foundNonClassType = true; - return; + } else if (TypeBase.isInstance(subtype) && ClassType.isBuiltIn(subtype, 'type')) { + if (subtype.priv.typeArgs && subtype.priv.typeArgs.length > 0) { + const typeArg = subtype.priv.typeArgs[0]; + if (isAnyOrUnknown(typeArg)) { + foundNonClassType = true; + return; + } + if (isInstantiableClass(typeArg)) { + subtype = typeArg; + } else if (isClass(typeArg) && TypeBase.isInstance(typeArg)) { + subtype = convertToInstantiable(typeArg); + } } - subtype = convertToInstantiable(subtype); } } if (isInstantiableClass(subtype)) { - if (subtype.priv.includeSubclasses) { - subtype = ClassType.cloneIncludeSubclasses(subtype, /* includeSubclasses */ false); - } // If this is a reference to a class that has type promotions (e.g. // float or complex), remove the promotions for purposes of the // isinstance check). - if (subtype.priv.includePromotions) { + if (!subtype.priv.includeSubclasses && subtype.priv.includePromotions) { subtype = ClassType.cloneRemoveTypePromotions(subtype); } classTypeList.push(subtype); @@ -1553,8 +1553,12 @@ function narrowTypeForInstance( // note this case specially so we don't do any narrowing, which // will generate false positives. if (filterIsSuperclass) { - if (!isTypeIsCheck && concreteFilterType.priv.includeSubclasses) { - // If the filter type includes subclasses, we can't eliminate + if ( + !isTypeIsCheck && + concreteFilterType.priv.includeSubclasses && + !ClassType.isFinal(concreteFilterType) + ) { + // If the filter type includes subclasses and is not final, we can't eliminate // this type in the negative direction. We'll relax this for // TypeIs checks. isClassRelationshipIndeterminate = true; diff --git a/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance22.py b/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance22.py index f90b182f8f6f..fef67ef0989b 100644 --- a/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance22.py +++ b/packages/pyright-internal/src/tests/samples/typeNarrowingIsinstance22.py @@ -3,6 +3,7 @@ # pyright: reportMissingModuleSource=false +from typing import final from typing_extensions import reveal_type @@ -14,34 +15,23 @@ class B: pass -class C: +@final +class FinalClass: pass -def test_tuple_param(x: A | B | C, types: tuple[type[A], type[B]]): - if isinstance(x, types): - reveal_type(x, expected_text="A | B") - else: - reveal_type(x, expected_text="C") +def test_positive_narrowing(x: A | B, cls: type[A]): + if isinstance(x, cls): + reveal_type(x, expected_text="A") -def test_tuple_annotated_local(x: A | B | C): - types: tuple[type[A], type[B]] = (A, B) +def test_positive_tuple_param(x: A | B, types: tuple[type[A], type[B]]): if isinstance(x, types): reveal_type(x, expected_text="A | B") - else: - reveal_type(x, expected_text="C") -def test_single_class_param(x: A | B, cls: type[A]): +def test_final_class_param(x: FinalClass | B, cls: type[FinalClass]): if isinstance(x, cls): - reveal_type(x, expected_text="A") + reveal_type(x, expected_text="FinalClass") else: reveal_type(x, expected_text="B") - - -def test_issubclass_param(sub_cls: type[A] | type[B], cls: type[A]): - if issubclass(sub_cls, cls): - reveal_type(sub_cls, expected_text="type[A]") - else: - reveal_type(sub_cls, expected_text="type[B]")