Better model runtime in isinstance and type checks (#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.
diff --git a/mypy/checker.py b/mypy/checker.py
index 2fdfdf7..d371f40 100644
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -6804,18 +6804,20 @@
continue
expr = operands[j]
- current_type_range = self.get_isinstance_type(expr)
- if current_type_range is not None:
- target_type = make_simplified_union([tr.item for tr in current_type_range])
- if isinstance(target_type, AnyType):
- # Avoid widening to Any for checks like `type(x) is type(y: Any)`.
- # We patch this here because it is desirable to widen to any for cases like
- # isinstance(x, (y: Any))
- continue
+ current_type_range = self.get_type_range_of_type(operand_types[j])
+ if current_type_range is None:
+ continue
+ if isinstance(get_proper_type(current_type_range.item), AnyType):
+ # Avoid widening to Any for checks like `type(x) is type(y: Any)`.
+ # We patch this here because it is desirable to widen to any for cases like
+ # isinstance(x, (y: Any))
+ continue
if_map, else_map = conditional_types_to_typemaps(
expr_in_type_expr,
*self.conditional_types_with_intersection(
- self.lookup_type(expr_in_type_expr), current_type_range, expr_in_type_expr
+ self.lookup_type(expr_in_type_expr),
+ [current_type_range],
+ expr_in_type_expr,
),
)
@@ -7899,51 +7901,87 @@
return first_item.var.is_settable_property
return False
- def get_isinstance_type(self, expr: Expression) -> list[TypeRange] | None:
+ def get_isinstance_type(
+ self, expr: Expression, flatten_tuples: bool = True
+ ) -> list[TypeRange] | None:
"""Get the type(s) resulting from an isinstance check.
Returns an empty list for isinstance(x, ()).
"""
if isinstance(expr, OpExpr) and expr.op == "|":
- left = self.get_isinstance_type(expr.left)
- if left is None and is_literal_none(expr.left):
+ left: list[TypeRange] | None
+ right: list[TypeRange] | None
+ if is_literal_none(expr.left):
left = [TypeRange(NoneType(), is_upper_bound=False)]
- right = self.get_isinstance_type(expr.right)
- if right is None and is_literal_none(expr.right):
+ else:
+ left = self.get_isinstance_type(expr.left, flatten_tuples=False)
+ if is_literal_none(expr.right):
right = [TypeRange(NoneType(), is_upper_bound=False)]
+ else:
+ right = self.get_isinstance_type(expr.right, flatten_tuples=False)
if left is None or right is None:
return None
return left + right
- all_types = get_proper_types(flatten_types(self.lookup_type(expr)))
- types: list[TypeRange] = []
- 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.
- any_parameterized = fill_typevars_with_any(typ.type_object())
- # Tuples may have unattended type variables among their items
- 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"
- # we indicate this by setting is_upper_bound flag
- is_upper_bound = True
- if isinstance(typ.item, NoneType):
- # except for Type[None], because "'NoneType' is not an acceptable base type"
- is_upper_bound = False
- types.append(TypeRange(typ.item, is_upper_bound=is_upper_bound))
- elif isinstance(typ, Instance) and typ.type.fullname == "builtins.type":
- object_type = Instance(typ.type.mro[-1], [])
- types.append(TypeRange(object_type, is_upper_bound=True))
- elif isinstance(typ, Instance) and typ.type.fullname == "types.UnionType" and typ.args:
- types.append(TypeRange(UnionType(typ.args), is_upper_bound=False))
- elif isinstance(typ, AnyType):
- types.append(TypeRange(typ, is_upper_bound=False))
- else: # we didn't see an actual type, but rather a variable with unknown value
+
+ if flatten_tuples:
+ type_ranges = []
+ for typ in flatten_types_if_tuple(self.lookup_type(expr)):
+ type_range = self.get_type_range_of_type(typ)
+ if type_range is None:
+ return None
+ type_ranges.append(type_range)
+ return type_ranges
+
+ else:
+ type_range = self.get_type_range_of_type(self.lookup_type(expr))
+ if type_range is None:
return None
- return types
+ return [type_range]
+
+ def get_type_range_of_type(self, typ: Type) -> TypeRange | None:
+ typ = get_proper_type(typ)
+ if isinstance(typ, TypeVarType):
+ typ = get_proper_type(typ.upper_bound)
+
+ if isinstance(typ, UnionType):
+ type_ranges = [self.get_type_range_of_type(item) for item in typ.items]
+ is_upper_bound = any(t.is_upper_bound for t in type_ranges if t is not None)
+ item = make_simplified_union([t.item for t in type_ranges if t is not None])
+ return TypeRange(item, is_upper_bound=is_upper_bound)
+ if isinstance(typ, FunctionLike) and typ.is_type_obj():
+ # If a type is generic, `isinstance` can only narrow its variables to Any.
+ any_parameterized = fill_typevars_with_any(typ.type_object())
+ # Tuples may have unattended type variables among their items
+ if isinstance(any_parameterized, TupleType):
+ erased_type = erase_typevars(any_parameterized)
+ else:
+ erased_type = any_parameterized
+ return TypeRange(erased_type, is_upper_bound=False)
+ if isinstance(typ, TypeType):
+ # Type[A] means "any type that is a subtype of A" rather than "precisely type A"
+ # we indicate this by setting is_upper_bound flag
+ is_upper_bound = True
+ if isinstance(typ.item, NoneType):
+ # except for Type[None], because "'NoneType' is not an acceptable base type"
+ is_upper_bound = False
+ return TypeRange(typ.item, is_upper_bound=is_upper_bound)
+ if isinstance(typ, AnyType):
+ return TypeRange(typ, is_upper_bound=False)
+ if isinstance(typ, Instance) and typ.type.fullname == "builtins.type":
+ object_type = Instance(typ.type.mro[-1], [])
+ return TypeRange(object_type, is_upper_bound=True)
+ if isinstance(typ, Instance) and typ.type.fullname == "types.UnionType" and typ.args:
+ return TypeRange(UnionType(typ.args), is_upper_bound=False)
+ if isinstance(typ, Instance) and typ.type.fullname == "typing._SpecialForm":
+ # This is probably an alias to a Union object. We don't have the args here so we can't
+ # conclude anything
+ return None
+ if not is_subtype(self.named_type("builtins.type"), typ):
+ # We saw something, but it couldn't possibly be valid
+ return TypeRange(UninhabitedType(), is_upper_bound=False)
+
+ # This is e.g. a variable of type object, so we can't conclude anything
+ return None
def is_literal_enum(self, n: Expression) -> bool:
"""Returns true if this expression (with the given type context) is an Enum literal.
@@ -8642,13 +8680,13 @@
return [t]
-def flatten_types(t: Type) -> list[Type]:
+def flatten_types_if_tuple(t: Type) -> list[Type]:
"""Flatten a nested sequence of tuples into one list of nodes."""
t = get_proper_type(t)
if isinstance(t, UnionType):
- return [b for a in t.items for b in flatten_types(a)]
+ return [b for a in t.items for b in flatten_types_if_tuple(a)]
if isinstance(t, TupleType):
- return [b for a in t.items for b in flatten_types(a)]
+ return [b for a in t.items for b in flatten_types_if_tuple(a)]
elif is_named_instance(t, "builtins.tuple"):
return [t.args[0]]
return [t]
diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test
index f27d905..c386555 100644
--- a/test-data/unit/check-isinstance.test
+++ b/test-data/unit/check-isinstance.test
@@ -1748,6 +1748,21 @@
cls()[0] + 1
[builtins fixtures/isinstancelist.pyi]
+[case testIssubclassTypeVar]
+# flags: --strict-equality --warn-unreachable
+from __future__ import annotations
+from typing import TypeVar
+
+ClassT = TypeVar("ClassT", bound=type)
+
+def directed_meet(cls0: ClassT, cls1: ClassT) -> ClassT | None:
+ if issubclass(cls1, cls0):
+ return cls1
+ if issubclass(cls0, cls1):
+ return cls0
+ return None
+[builtins fixtures/isinstancelist.pyi]
+
[case testIsinstanceTypeArgs]
from typing import Iterable, TypeVar
x = 1
@@ -2932,6 +2947,7 @@
[builtins fixtures/isinstance.pyi]
[case testIsInstanceTypeAny]
+# flags: --strict-equality --warn-unreachable
from typing import Any
def foo(x: object, t: type[Any]):
@@ -2939,10 +2955,36 @@
reveal_type(x) # N: Revealed type is "Any"
[builtins fixtures/isinstance.pyi]
+[case testIsInstanceObject]
+# flags: --strict-equality --warn-unreachable
+from typing import Any
+
+def foo(x: object, t: object):
+ if isinstance(x, t): # E: Argument 2 to "isinstance" has incompatible type "object"; expected "type[object] | tuple[type[object], ...]"
+ reveal_type(x) # N: Revealed type is "builtins.object"
+[builtins fixtures/isinstance.pyi]
+
+[case testIsInstanceOrExprInTuple]
+# flags: --strict-equality --warn-unreachable
+from typing import Any
+
+def f1(x: object):
+ if isinstance(x, str | (int, dict)): # E: Argument 2 to "isinstance" has incompatible type "object"; expected "type | tuple[Any, ...]"
+ reveal_type(x) # N: Revealed type is "builtins.str"
+ if type(x) == str | (int, dict):
+ reveal_type(x) # N: Revealed type is "builtins.object"
+
+def f2(x: Any):
+ if isinstance(x, str | (int, dict)): # E: Argument 2 to "isinstance" has incompatible type "object"; expected "type | tuple[Any, ...]"
+ reveal_type(x) # N: Revealed type is "builtins.str"
+ if type(x) == str | (int, dict):
+ reveal_type(x) # N: Revealed type is "Any"
+[builtins fixtures/primitives.pyi]
+
[case testIsInstanceUnionOfTuples]
# flags: --strict-equality --warn-unreachable
from __future__ import annotations
-from typing import TypeVar, Iterator
+from typing import TypeVar, Iterator, final
T1 = TypeVar("T1")
T2 = TypeVar("T2")
@@ -2960,4 +3002,53 @@
reveal_type(values) # N: Revealed type is "T1`-1 | T2`-2 | T3`-3"
yield values
raise
+
+class A: ...
+class B: ...
+
+def f1(x: A | B, t: tuple[type[A]] | tuple[type[B]]):
+ if isinstance(x, t):
+ reveal_type(x) # N: Revealed type is "__main__.A | __main__.B"
+ else:
+ reveal_type(x) # N: Revealed type is "__main__.A | __main__.B"
+
+def f2(x: object, t: tuple[type[A]] | tuple[type[B]]):
+ if isinstance(x, t):
+ reveal_type(x) # N: Revealed type is "__main__.A | __main__.B"
+ else:
+ reveal_type(x) # N: Revealed type is "builtins.object"
+
+@final
+class FA: ...
+@final
+class FB: ...
+
+def g1(x: FA | FB, t: tuple[type[FA]] | tuple[type[FB]]):
+ if isinstance(x, t):
+ reveal_type(x) # N: Revealed type is "__main__.FA | __main__.FB"
+ else:
+ reveal_type(x) # N: Revealed type is "__main__.FA | __main__.FB"
+
+def g2(x: object, t: tuple[type[FA]] | tuple[type[FB]]):
+ if isinstance(x, t):
+ reveal_type(x) # N: Revealed type is "__main__.FA | __main__.FB"
+ else:
+ reveal_type(x) # N: Revealed type is "builtins.object"
+[builtins fixtures/primitives.pyi]
+
+
+[case testIsInstanceTypeVarBoundToType]
+# flags: --strict-equality --warn-unreachable
+from __future__ import annotations
+from typing import TypeVar, Protocol
+
+class A(Protocol):
+ x: int
+
+T = TypeVar("T", bound=type[A])
+
+def foo(x: object, t: T):
+ if isinstance(x, t):
+ reveal_type(x) # N: Revealed type is "__main__.A"
+ reveal_type(x.x) # N: Revealed type is "builtins.int"
[builtins fixtures/primitives.pyi]
diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test
index 368ce81..b9bb0c1 100644
--- a/test-data/unit/check-narrowing.test
+++ b/test-data/unit/check-narrowing.test
@@ -3180,13 +3180,22 @@
reveal_type(y) # N: Revealed type is "collections.defaultdict[Any, Any]"
reveal_type(z) # N: Revealed type is "collections.defaultdict[Any, Any]"
-[case testUnionTypeEquality-xfail]
+[case testTypeEqualsTypeObjectUnion]
+# flags: --strict-equality --warn-unreachable
+from __future__ import annotations
+def f(x: object, y: type[int] | type[str]):
+ if type(x) == y:
+ reveal_type(x) # N: Revealed type is "builtins.int | builtins.str"
+ reveal_type(y) # N: Revealed type is "type[builtins.int] | type[builtins.str]"
+[builtins fixtures/primitives.pyi]
+
+[case testUnionTypeEquality]
# flags: --strict-equality --warn-unreachable
from typing import Any, reveal_type
-x: Any = ()
-if type(x) == (int, str):
- reveal_type(x) # E: Statement is unreachable
+def f(x: Any):
+ if type(x) == (int, str): # E: Non-overlapping equality check (left operand type: "type[Any]", right operand type: "tuple[type[int], type[str]]")
+ reveal_type(x) # E: Statement is unreachable
[builtins fixtures/tuple.pyi]
[case testTypeIntersectionWithConcreteTypes]