Repository: python/mypy

Issue:
Strict optional: Any narrowing to None in a conditional gets "stuck" at None
I think a code sample will explain:

```
from typing import Any, Optional

def narrow_any_to_none() -> None:
    """Broken case: If 'v' starts with type Any, then it gets "stuck" with type
    None after the if."""
    v = 1 # type: Any

    if v is None:
        reveal_type(v) # E: Revealed type is 'builtins.None'
        v = 2
        reveal_type(v) # E: Revealed type is 'builtins.None'
        v + 3 # E: Unsupported operand types for + (None and "int")


def narrow_optional_to_none() -> None:
    """Working as expected: If 'v' starts with an Optional type, then you can
    re-assign it no problem after the if."""
    v = 1 # type: Optional[int]

    if v is None:
        reveal_type(v) # E: Revealed type is 'builtins.None'
        v = 2
        reveal_type(v) # E: Revealed type is 'builtins.int'
        v + 3
```

In v0.510 and on the latest master, `mypy --strict-optional` on the above code produces the above errors. I expected the 2nd reveal_type to reveal 'builtins.int' and for the v + 3 line to pass type checking. (Without strict optional, everything works as expected.)


Discussion:
@ddfisher Looks like a binder bug?

Resolving patch:
diff --git a/mypy/binder.py b/mypy/binder.py
--- a/mypy/binder.py
+++ b/mypy/binder.py
@@ -221,12 +221,16 @@ def assign_type(self, expr: Expression,
             # times?
             return
 
-        # If x is Any and y is int, after x = y we do not infer that x is int.
-        # This could be changed.
-
-        if (isinstance(self.most_recent_enclosing_type(expr, type), AnyType)
+        enclosing_type = self.most_recent_enclosing_type(expr, type)
+        if (isinstance(enclosing_type, AnyType)
                 and not restrict_any):
-            pass
+            # If x is Any and y is int, after x = y we do not infer that x is int.
+            # This could be changed.
+            if not isinstance(type, AnyType):
+                # We narrowed type from Any in a recent frame (probably an
+                # isinstance check), but now it is reassigned, so broaden back
+                # to Any (which is the most recent enclosing type)
+                self.put(expr, enclosing_type)
         elif (isinstance(type, AnyType)
               and not (isinstance(declared_type, UnionType)
                        and any(isinstance(item, AnyType) for item in declared_type.items))):


Test patch:
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
@@ -1712,3 +1712,17 @@ if isinstance(x, (set, (list, T))):
     reveal_type(x)  # E: Revealed type is 'Union[builtins.set[Any], builtins.list[Any], builtins.int, builtins.str]'
 
 [builtins fixtures/isinstancelist.pyi]
+
+
+[case testIsinstanceNarrowAny]
+from typing import Any
+
+def narrow_any_to_str_then_reassign_to_int() -> None:
+    v = 1 # type: Any
+
+    if isinstance(v, str):
+        reveal_type(v)  # E: Revealed type is 'builtins.str'
+        v = 2
+        reveal_type(v)  # E: Revealed type is 'Any'
+
+[builtins fixtures/isinstance.pyi]