Skip to content

Commit eba9a4c

Browse files
authored
Better model runtime in isinstance and type checks (python#20675)
We now more accurately model reachability for impossible checks. We also avoid allowing tuples and X | Y syntax in situations where they don't work at runtime.
1 parent 09283a7 commit eba9a4c

File tree

3 files changed

+189
-51
lines changed

3 files changed

+189
-51
lines changed

mypy/checker.py

Lines changed: 84 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6804,18 +6804,20 @@ def narrow_type_by_identity_equality(
68046804
continue
68056805
expr = operands[j]
68066806

6807-
current_type_range = self.get_isinstance_type(expr)
6808-
if current_type_range is not None:
6809-
target_type = make_simplified_union([tr.item for tr in current_type_range])
6810-
if isinstance(target_type, AnyType):
6811-
# Avoid widening to Any for checks like `type(x) is type(y: Any)`.
6812-
# We patch this here because it is desirable to widen to any for cases like
6813-
# isinstance(x, (y: Any))
6814-
continue
6807+
current_type_range = self.get_type_range_of_type(operand_types[j])
6808+
if current_type_range is None:
6809+
continue
6810+
if isinstance(get_proper_type(current_type_range.item), AnyType):
6811+
# Avoid widening to Any for checks like `type(x) is type(y: Any)`.
6812+
# We patch this here because it is desirable to widen to any for cases like
6813+
# isinstance(x, (y: Any))
6814+
continue
68156815
if_map, else_map = conditional_types_to_typemaps(
68166816
expr_in_type_expr,
68176817
*self.conditional_types_with_intersection(
6818-
self.lookup_type(expr_in_type_expr), current_type_range, expr_in_type_expr
6818+
self.lookup_type(expr_in_type_expr),
6819+
[current_type_range],
6820+
expr_in_type_expr,
68196821
),
68206822
)
68216823

@@ -7899,51 +7901,87 @@ def is_writable_attribute(self, node: Node) -> bool:
78997901
return first_item.var.is_settable_property
79007902
return False
79017903

7902-
def get_isinstance_type(self, expr: Expression) -> list[TypeRange] | None:
7904+
def get_isinstance_type(
7905+
self, expr: Expression, flatten_tuples: bool = True
7906+
) -> list[TypeRange] | None:
79037907
"""Get the type(s) resulting from an isinstance check.
79047908
79057909
Returns an empty list for isinstance(x, ()).
79067910
"""
79077911
if isinstance(expr, OpExpr) and expr.op == "|":
7908-
left = self.get_isinstance_type(expr.left)
7909-
if left is None and is_literal_none(expr.left):
7912+
left: list[TypeRange] | None
7913+
right: list[TypeRange] | None
7914+
if is_literal_none(expr.left):
79107915
left = [TypeRange(NoneType(), is_upper_bound=False)]
7911-
right = self.get_isinstance_type(expr.right)
7912-
if right is None and is_literal_none(expr.right):
7916+
else:
7917+
left = self.get_isinstance_type(expr.left, flatten_tuples=False)
7918+
if is_literal_none(expr.right):
79137919
right = [TypeRange(NoneType(), is_upper_bound=False)]
7920+
else:
7921+
right = self.get_isinstance_type(expr.right, flatten_tuples=False)
79147922
if left is None or right is None:
79157923
return None
79167924
return left + right
7917-
all_types = get_proper_types(flatten_types(self.lookup_type(expr)))
7918-
types: list[TypeRange] = []
7919-
for typ in all_types:
7920-
if isinstance(typ, FunctionLike) and typ.is_type_obj():
7921-
# If a type is generic, `isinstance` can only narrow its variables to Any.
7922-
any_parameterized = fill_typevars_with_any(typ.type_object())
7923-
# Tuples may have unattended type variables among their items
7924-
if isinstance(any_parameterized, TupleType):
7925-
erased_type = erase_typevars(any_parameterized)
7926-
else:
7927-
erased_type = any_parameterized
7928-
types.append(TypeRange(erased_type, is_upper_bound=False))
7929-
elif isinstance(typ, TypeType):
7930-
# Type[A] means "any type that is a subtype of A" rather than "precisely type A"
7931-
# we indicate this by setting is_upper_bound flag
7932-
is_upper_bound = True
7933-
if isinstance(typ.item, NoneType):
7934-
# except for Type[None], because "'NoneType' is not an acceptable base type"
7935-
is_upper_bound = False
7936-
types.append(TypeRange(typ.item, is_upper_bound=is_upper_bound))
7937-
elif isinstance(typ, Instance) and typ.type.fullname == "builtins.type":
7938-
object_type = Instance(typ.type.mro[-1], [])
7939-
types.append(TypeRange(object_type, is_upper_bound=True))
7940-
elif isinstance(typ, Instance) and typ.type.fullname == "types.UnionType" and typ.args:
7941-
types.append(TypeRange(UnionType(typ.args), is_upper_bound=False))
7942-
elif isinstance(typ, AnyType):
7943-
types.append(TypeRange(typ, is_upper_bound=False))
7944-
else: # we didn't see an actual type, but rather a variable with unknown value
7925+
7926+
if flatten_tuples:
7927+
type_ranges = []
7928+
for typ in flatten_types_if_tuple(self.lookup_type(expr)):
7929+
type_range = self.get_type_range_of_type(typ)
7930+
if type_range is None:
7931+
return None
7932+
type_ranges.append(type_range)
7933+
return type_ranges
7934+
7935+
else:
7936+
type_range = self.get_type_range_of_type(self.lookup_type(expr))
7937+
if type_range is None:
79457938
return None
7946-
return types
7939+
return [type_range]
7940+
7941+
def get_type_range_of_type(self, typ: Type) -> TypeRange | None:
7942+
typ = get_proper_type(typ)
7943+
if isinstance(typ, TypeVarType):
7944+
typ = get_proper_type(typ.upper_bound)
7945+
7946+
if isinstance(typ, UnionType):
7947+
type_ranges = [self.get_type_range_of_type(item) for item in typ.items]
7948+
is_upper_bound = any(t.is_upper_bound for t in type_ranges if t is not None)
7949+
item = make_simplified_union([t.item for t in type_ranges if t is not None])
7950+
return TypeRange(item, is_upper_bound=is_upper_bound)
7951+
if isinstance(typ, FunctionLike) and typ.is_type_obj():
7952+
# If a type is generic, `isinstance` can only narrow its variables to Any.
7953+
any_parameterized = fill_typevars_with_any(typ.type_object())
7954+
# Tuples may have unattended type variables among their items
7955+
if isinstance(any_parameterized, TupleType):
7956+
erased_type = erase_typevars(any_parameterized)
7957+
else:
7958+
erased_type = any_parameterized
7959+
return TypeRange(erased_type, is_upper_bound=False)
7960+
if isinstance(typ, TypeType):
7961+
# Type[A] means "any type that is a subtype of A" rather than "precisely type A"
7962+
# we indicate this by setting is_upper_bound flag
7963+
is_upper_bound = True
7964+
if isinstance(typ.item, NoneType):
7965+
# except for Type[None], because "'NoneType' is not an acceptable base type"
7966+
is_upper_bound = False
7967+
return TypeRange(typ.item, is_upper_bound=is_upper_bound)
7968+
if isinstance(typ, AnyType):
7969+
return TypeRange(typ, is_upper_bound=False)
7970+
if isinstance(typ, Instance) and typ.type.fullname == "builtins.type":
7971+
object_type = Instance(typ.type.mro[-1], [])
7972+
return TypeRange(object_type, is_upper_bound=True)
7973+
if isinstance(typ, Instance) and typ.type.fullname == "types.UnionType" and typ.args:
7974+
return TypeRange(UnionType(typ.args), is_upper_bound=False)
7975+
if isinstance(typ, Instance) and typ.type.fullname == "typing._SpecialForm":
7976+
# This is probably an alias to a Union object. We don't have the args here so we can't
7977+
# conclude anything
7978+
return None
7979+
if not is_subtype(self.named_type("builtins.type"), typ):
7980+
# We saw something, but it couldn't possibly be valid
7981+
return TypeRange(UninhabitedType(), is_upper_bound=False)
7982+
7983+
# This is e.g. a variable of type object, so we can't conclude anything
7984+
return None
79477985

79487986
def is_literal_enum(self, n: Expression) -> bool:
79497987
"""Returns true if this expression (with the given type context) is an Enum literal.
@@ -8642,13 +8680,13 @@ def flatten(t: Expression) -> list[Expression]:
86428680
return [t]
86438681

86448682

8645-
def flatten_types(t: Type) -> list[Type]:
8683+
def flatten_types_if_tuple(t: Type) -> list[Type]:
86468684
"""Flatten a nested sequence of tuples into one list of nodes."""
86478685
t = get_proper_type(t)
86488686
if isinstance(t, UnionType):
8649-
return [b for a in t.items for b in flatten_types(a)]
8687+
return [b for a in t.items for b in flatten_types_if_tuple(a)]
86508688
if isinstance(t, TupleType):
8651-
return [b for a in t.items for b in flatten_types(a)]
8689+
return [b for a in t.items for b in flatten_types_if_tuple(a)]
86528690
elif is_named_instance(t, "builtins.tuple"):
86538691
return [t.args[0]]
86548692
return [t]

test-data/unit/check-isinstance.test

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1748,6 +1748,21 @@ def f(cls: Type[object]) -> None:
17481748
cls()[0] + 1
17491749
[builtins fixtures/isinstancelist.pyi]
17501750

1751+
[case testIssubclassTypeVar]
1752+
# flags: --strict-equality --warn-unreachable
1753+
from __future__ import annotations
1754+
from typing import TypeVar
1755+
1756+
ClassT = TypeVar("ClassT", bound=type)
1757+
1758+
def directed_meet(cls0: ClassT, cls1: ClassT) -> ClassT | None:
1759+
if issubclass(cls1, cls0):
1760+
return cls1
1761+
if issubclass(cls0, cls1):
1762+
return cls0
1763+
return None
1764+
[builtins fixtures/isinstancelist.pyi]
1765+
17511766
[case testIsinstanceTypeArgs]
17521767
from typing import Iterable, TypeVar
17531768
x = 1
@@ -2932,17 +2947,44 @@ if isinstance(a, B):
29322947
[builtins fixtures/isinstance.pyi]
29332948

29342949
[case testIsInstanceTypeAny]
2950+
# flags: --strict-equality --warn-unreachable
29352951
from typing import Any
29362952

29372953
def foo(x: object, t: type[Any]):
29382954
if isinstance(x, t):
29392955
reveal_type(x) # N: Revealed type is "Any"
29402956
[builtins fixtures/isinstance.pyi]
29412957

2958+
[case testIsInstanceObject]
2959+
# flags: --strict-equality --warn-unreachable
2960+
from typing import Any
2961+
2962+
def foo(x: object, t: object):
2963+
if isinstance(x, t): # E: Argument 2 to "isinstance" has incompatible type "object"; expected "type[object] | tuple[type[object], ...]"
2964+
reveal_type(x) # N: Revealed type is "builtins.object"
2965+
[builtins fixtures/isinstance.pyi]
2966+
2967+
[case testIsInstanceOrExprInTuple]
2968+
# flags: --strict-equality --warn-unreachable
2969+
from typing import Any
2970+
2971+
def f1(x: object):
2972+
if isinstance(x, str | (int, dict)): # E: Argument 2 to "isinstance" has incompatible type "object"; expected "type | tuple[Any, ...]"
2973+
reveal_type(x) # N: Revealed type is "builtins.str"
2974+
if type(x) == str | (int, dict):
2975+
reveal_type(x) # N: Revealed type is "builtins.object"
2976+
2977+
def f2(x: Any):
2978+
if isinstance(x, str | (int, dict)): # E: Argument 2 to "isinstance" has incompatible type "object"; expected "type | tuple[Any, ...]"
2979+
reveal_type(x) # N: Revealed type is "builtins.str"
2980+
if type(x) == str | (int, dict):
2981+
reveal_type(x) # N: Revealed type is "Any"
2982+
[builtins fixtures/primitives.pyi]
2983+
29422984
[case testIsInstanceUnionOfTuples]
29432985
# flags: --strict-equality --warn-unreachable
29442986
from __future__ import annotations
2945-
from typing import TypeVar, Iterator
2987+
from typing import TypeVar, Iterator, final
29462988

29472989
T1 = TypeVar("T1")
29482990
T2 = TypeVar("T2")
@@ -2960,4 +3002,53 @@ def extract(
29603002
reveal_type(values) # N: Revealed type is "T1`-1 | T2`-2 | T3`-3"
29613003
yield values
29623004
raise
3005+
3006+
class A: ...
3007+
class B: ...
3008+
3009+
def f1(x: A | B, t: tuple[type[A]] | tuple[type[B]]):
3010+
if isinstance(x, t):
3011+
reveal_type(x) # N: Revealed type is "__main__.A | __main__.B"
3012+
else:
3013+
reveal_type(x) # N: Revealed type is "__main__.A | __main__.B"
3014+
3015+
def f2(x: object, t: tuple[type[A]] | tuple[type[B]]):
3016+
if isinstance(x, t):
3017+
reveal_type(x) # N: Revealed type is "__main__.A | __main__.B"
3018+
else:
3019+
reveal_type(x) # N: Revealed type is "builtins.object"
3020+
3021+
@final
3022+
class FA: ...
3023+
@final
3024+
class FB: ...
3025+
3026+
def g1(x: FA | FB, t: tuple[type[FA]] | tuple[type[FB]]):
3027+
if isinstance(x, t):
3028+
reveal_type(x) # N: Revealed type is "__main__.FA | __main__.FB"
3029+
else:
3030+
reveal_type(x) # N: Revealed type is "__main__.FA | __main__.FB"
3031+
3032+
def g2(x: object, t: tuple[type[FA]] | tuple[type[FB]]):
3033+
if isinstance(x, t):
3034+
reveal_type(x) # N: Revealed type is "__main__.FA | __main__.FB"
3035+
else:
3036+
reveal_type(x) # N: Revealed type is "builtins.object"
3037+
[builtins fixtures/primitives.pyi]
3038+
3039+
3040+
[case testIsInstanceTypeVarBoundToType]
3041+
# flags: --strict-equality --warn-unreachable
3042+
from __future__ import annotations
3043+
from typing import TypeVar, Protocol
3044+
3045+
class A(Protocol):
3046+
x: int
3047+
3048+
T = TypeVar("T", bound=type[A])
3049+
3050+
def foo(x: object, t: T):
3051+
if isinstance(x, t):
3052+
reveal_type(x) # N: Revealed type is "__main__.A"
3053+
reveal_type(x.x) # N: Revealed type is "builtins.int"
29633054
[builtins fixtures/primitives.pyi]

test-data/unit/check-narrowing.test

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3180,13 +3180,22 @@ if type(x) is type(y) is type(z):
31803180
reveal_type(y) # N: Revealed type is "collections.defaultdict[Any, Any]"
31813181
reveal_type(z) # N: Revealed type is "collections.defaultdict[Any, Any]"
31823182

3183-
[case testUnionTypeEquality-xfail]
3183+
[case testTypeEqualsTypeObjectUnion]
3184+
# flags: --strict-equality --warn-unreachable
3185+
from __future__ import annotations
3186+
def f(x: object, y: type[int] | type[str]):
3187+
if type(x) == y:
3188+
reveal_type(x) # N: Revealed type is "builtins.int | builtins.str"
3189+
reveal_type(y) # N: Revealed type is "type[builtins.int] | type[builtins.str]"
3190+
[builtins fixtures/primitives.pyi]
3191+
3192+
[case testUnionTypeEquality]
31843193
# flags: --strict-equality --warn-unreachable
31853194
from typing import Any, reveal_type
31863195

3187-
x: Any = ()
3188-
if type(x) == (int, str):
3189-
reveal_type(x) # E: Statement is unreachable
3196+
def f(x: Any):
3197+
if type(x) == (int, str): # E: Non-overlapping equality check (left operand type: "type[Any]", right operand type: "tuple[type[int], type[str]]")
3198+
reveal_type(x) # E: Statement is unreachable
31903199
[builtins fixtures/tuple.pyi]
31913200

31923201
[case testTypeIntersectionWithConcreteTypes]

0 commit comments

Comments
 (0)