From dc2868ece08119b2c510c4c1088ada327645264b Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 4 Jun 2025 01:48:25 +0200 Subject: [PATCH 1/3] Replace "try to extract type from the first overload" with simply using the underlying type --- mypy/checker.py | 5 ++- test-data/unit/check-narrowing.test | 47 +++++++++++++++++++++++++++++ test-data/unit/check-typeddict.test | 2 +- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 5201037242ac..e24168ab4fab 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7691,9 +7691,8 @@ def get_isinstance_type(self, expr: Expression) -> list[TypeRange] | None: types: list[TypeRange] = [] for typ in all_types: if isinstance(typ, FunctionLike) and typ.is_type_obj(): - # Type variables may be present -- erase them, which is the best - # we can do (outside disallowing them here). - erased_type = erase_typevars(typ.items[0].ret_type) + # If a type is generic, `isinstance` can only narrow its variables to Any. + erased_type = fill_typevars_with_any(typ.type_object()) types.append(TypeRange(erased_type, is_upper_bound=False)) elif isinstance(typ, TypeType): # Type[A] means "any type that is a subtype of A" rather than "precisely type A" diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 36b2ced075d2..0a1e63603f31 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2463,3 +2463,50 @@ def test(x: T) -> T: reveal_type(x.x) # N: Revealed type is "builtins.str" return x [builtins fixtures/isinstance.pyi] + +[case testIsinstanceNarrowingWithSelfTypes] +from typing import Generic, TypeVar, overload + +T = TypeVar("T") + +class A(Generic[T]): + def __init__(self: A[int]) -> None: + pass + +def check_a(obj: "A[T] | str") -> None: + reveal_type(obj) # N: Revealed type is "Union[__main__.A[T`-1], builtins.str]" + if isinstance(obj, A): + reveal_type(obj) # N: Revealed type is "__main__.A[T`-1]" + else: + reveal_type(obj) # N: Revealed type is "builtins.str" + +class B(Generic[T]): + @overload + def __init__(self, x: T) -> None: ... + @overload + def __init__(self: B[int]) -> None: ... + def __init__(self, x: "T | None" = None) -> None: + pass + +def check_b(obj: "B[T] | str") -> None: + reveal_type(obj) # N: Revealed type is "Union[__main__.B[T`-1], builtins.str]" + if isinstance(obj, B): + reveal_type(obj) # N: Revealed type is "__main__.B[T`-1]" + else: + reveal_type(obj) # N: Revealed type is "builtins.str" + +class C(Generic[T]): + @overload + def __init__(self: C[int]) -> None: ... + @overload + def __init__(self, x: T) -> None: ... + def __init__(self, x: "T | None" = None) -> None: + pass + +def check_c(obj: "C[T] | str") -> None: + reveal_type(obj) # N: Revealed type is "Union[__main__.C[T`-1], builtins.str]" + if isinstance(obj, C): + reveal_type(obj) # N: Revealed type is "__main__.C[T`-1]" + else: + reveal_type(obj) # N: Revealed type is "builtins.str" +[builtins fixtures/isinstance.pyi] diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 4ac69321a250..6bcc6e20328b 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -810,7 +810,7 @@ from typing import TypedDict D = TypedDict('D', {'x': int}) d: object if isinstance(d, D): # E: Cannot use isinstance() with TypedDict type - reveal_type(d) # N: Revealed type is "TypedDict('__main__.D', {'x': builtins.int})" + reveal_type(d) # N: Revealed type is "__main__.D" issubclass(object, D) # E: Cannot use issubclass() with TypedDict type [builtins fixtures/isinstancelist.pyi] [typing fixtures/typing-typeddict.pyi] From 4fe7d9f8e2b47d95b39426a4ea5c4b39e4b229c6 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 4 Jun 2025 03:28:59 +0200 Subject: [PATCH 2/3] Handle tuples specially --- mypy/checker.py | 3 +++ test-data/unit/check-narrowing.test | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index e24168ab4fab..d37f31537101 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7693,6 +7693,9 @@ def get_isinstance_type(self, expr: Expression) -> list[TypeRange] | None: if isinstance(typ, FunctionLike) and typ.is_type_obj(): # If a type is generic, `isinstance` can only narrow its variables to Any. erased_type = fill_typevars_with_any(typ.type_object()) + # Tuples may have unattended type variables among their items + if isinstance(erased_type, TupleType): + erased_type = erase_typevars(erased_type) types.append(TypeRange(erased_type, is_upper_bound=False)) elif isinstance(typ, TypeType): # Type[A] means "any type that is a subtype of A" rather than "precisely type A" diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 0a1e63603f31..a5c8f53b9726 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2480,6 +2480,7 @@ def check_a(obj: "A[T] | str") -> None: else: reveal_type(obj) # N: Revealed type is "builtins.str" + class B(Generic[T]): @overload def __init__(self, x: T) -> None: ... @@ -2495,6 +2496,7 @@ def check_b(obj: "B[T] | str") -> None: else: reveal_type(obj) # N: Revealed type is "builtins.str" + class C(Generic[T]): @overload def __init__(self: C[int]) -> None: ... @@ -2509,4 +2511,12 @@ def check_c(obj: "C[T] | str") -> None: reveal_type(obj) # N: Revealed type is "__main__.C[T`-1]" else: reveal_type(obj) # N: Revealed type is "builtins.str" -[builtins fixtures/isinstance.pyi] + + +class D(tuple[T], Generic[T]): ... + +def check_d(arg: D[T]) -> None: + if not isinstance(arg, D): + return + reveal_type(arg) # N: Revealed type is "tuple[T`-1, fallback=__main__.D[Any]]" +[builtins fixtures/tuple.pyi] From 7b112567cc4ba36c51df8ce927c5f782d4a174ad Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 4 Jun 2025 03:44:30 +0200 Subject: [PATCH 3/3] Fix own typing --- mypy/checker.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index d37f31537101..2686c22dded4 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7692,10 +7692,12 @@ def get_isinstance_type(self, expr: Expression) -> list[TypeRange] | None: for typ in all_types: if isinstance(typ, FunctionLike) and typ.is_type_obj(): # If a type is generic, `isinstance` can only narrow its variables to Any. - erased_type = fill_typevars_with_any(typ.type_object()) + any_parameterized = fill_typevars_with_any(typ.type_object()) # Tuples may have unattended type variables among their items - if isinstance(erased_type, TupleType): - erased_type = erase_typevars(erased_type) + if isinstance(any_parameterized, TupleType): + erased_type = erase_typevars(any_parameterized) + else: + erased_type = any_parameterized types.append(TypeRange(erased_type, is_upper_bound=False)) elif isinstance(typ, TypeType): # Type[A] means "any type that is a subtype of A" rather than "precisely type A"