Repository: python/mypy

Issue:
assert isinstance can widen types
Here's an example:
```py
x = ""  # type: str
reveal_type(x)  # E: Revealed type is 'builtins.str'
assert isinstance(x, (int, str))
reveal_type(x)  # E: Revealed type is 'Union[builtins.int, builtins.str]'
```

We should limit `assert isinstance(...)` to strictly narrowing types instead.
assert isinstance can widen types
Here's an example:
```py
x = ""  # type: str
reveal_type(x)  # E: Revealed type is 'builtins.str'
assert isinstance(x, (int, str))
reveal_type(x)  # E: Revealed type is 'Union[builtins.int, builtins.str]'
```

We should limit `assert isinstance(...)` to strictly narrowing types instead.


Discussion:
Looking around at the source, is there a reason `is_proper_subtype` doesn't consider unions at all? Or was it just an oversight?

I have a fix, but I'm just not sure where it should go exactly...
Likely to be an oversight.  I think `is_proper_subtype` was never really fully implemented...
Looking around at the source, is there a reason `is_proper_subtype` doesn't consider unions at all? Or was it just an oversight?

I have a fix, but I'm just not sure where it should go exactly...
Likely to be an oversight.  I think `is_proper_subtype` was never really fully implemented...

Resolving patch:
diff --git a/mypy/subtypes.py b/mypy/subtypes.py
--- a/mypy/subtypes.py
+++ b/mypy/subtypes.py
@@ -514,6 +514,12 @@ def is_proper_subtype(t: Type, s: Type) -> bool:
     Any types. Any instance type t is also a proper subtype of t.
     """
     # FIX tuple types
+    if isinstance(s, UnionType):
+        if isinstance(t, UnionType):
+            return all([is_proper_subtype(item, s) for item in t.items])
+        else:
+            return any([is_proper_subtype(t, item) for item in s.items])
+
     if isinstance(t, Instance):
         if isinstance(s, Instance):
             if not t.type.has_base(s.type.fullname()):


Test patch:
diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py
--- a/mypy/test/testtypes.py
+++ b/mypy/test/testtypes.py
@@ -210,6 +210,12 @@ def test_is_proper_subtype(self) -> None:
         assert_true(is_proper_subtype(fx.t, fx.t))
         assert_false(is_proper_subtype(fx.t, fx.s))
 
+        assert_true(is_proper_subtype(fx.a, UnionType([fx.a, fx.b])))
+        assert_true(is_proper_subtype(UnionType([fx.a, fx.b]),
+                                      UnionType([fx.a, fx.b, fx.c])))
+        assert_false(is_proper_subtype(UnionType([fx.a, fx.b]),
+                                       UnionType([fx.b, fx.c])))
+
     def test_is_proper_subtype_covariance(self) -> None:
         fx_co = self.fx_co
 
diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test
--- a/test-data/unit/check-isinstance.test
+++ b/test-data/unit/check-isinstance.test
@@ -534,6 +534,22 @@ if isinstance(v, (B, C)):
     v.method3('xyz') # E: Some element of union has no attribute "method3"
 [builtins fixtures/isinstance.pyi]
 
+[case testIsinstanceNeverWidens]
+from typing import Union
+
+class A: pass
+class B: pass
+class C: pass
+
+a = A()  # type: A
+assert isinstance(a, (A, B))
+reveal_type(a)  # E: Revealed type is '__main__.A'
+
+b = A()  # type: Union[A, B]
+assert isinstance(b, (A, B, C))
+reveal_type(b)  # E: Revealed type is 'Union[__main__.A, __main__.B]'
+[builtins fixtures/isinstance.pyi]
+
 [case testMemberAssignmentChanges-skip]
 from typing import Union