Repository: python/mypy

Issue:
Variable redefinition (--allow-redefinition) not working inside with-block
* Are you reporting a bug, or opening a feature request?
Bug

* Please insert below the code you are checking with mypy,
  or a mock-up repro if the source is private. We would appreciate
  if you try to simplify your case to a minimal repro.
```python
with open('file.txt') as f:
    number = '123'
    number = int(number)
```

* What is the actual behavior/output?
```
$ mypy --allow-redefinition test.py
test.py:3: error: Incompatible types in assignment (expression has type "int", variable has type "str")
```
* What is the behavior/output you expect?
No errors; `number`'s type should be redefined to `int`.

* What are the versions of mypy and Python you are using?
  Do you see the same issue after installing mypy from Git master?
mypy 0.710+dev.f10528a5a8d9e98939ceb84d40e614d1f159d9a0 (master), Python 3.7.3

* What are the mypy flags you are using? (For example --strict-optional)
`--allow-redefinition`

* Note: no errors are thrown after removing the with-block:
```python
f = open('file.txt')
number = '123'
number = int(number)
```


Discussion:
Hm, I think this should be allowed. It is also prohibited in `try ... except` blocks, but that makes sense because "non-linear" flow is expected there, while exceptions are rarely "swallowed" (or even expected) in `with` blocks.

Anyway, assigning @JukkaL for final decision.

Resolving patch:
diff --git a/mypy/renaming.py b/mypy/renaming.py
--- a/mypy/renaming.py
+++ b/mypy/renaming.py
@@ -146,9 +146,9 @@ def visit_try_stmt(self, stmt: TryStmt) -> None:
         # Variables defined by a try statement get special treatment in the
         # type checker which allows them to be always redefined, so no need to
         # do renaming here.
-        self.enter_with_or_try()
+        self.enter_try()
         super().visit_try_stmt(stmt)
-        self.leave_with_or_try()
+        self.leave_try()
 
     def visit_with_stmt(self, stmt: WithStmt) -> None:
         for expr in stmt.expr:
@@ -156,9 +156,13 @@ def visit_with_stmt(self, stmt: WithStmt) -> None:
         for target in stmt.target:
             if target is not None:
                 self.analyze_lvalue(target)
-        self.enter_with_or_try()
+        # We allow redefinitions in the body of a with statement for
+        # convenience.  This is unsafe since with statements can affect control
+        # flow by catching exceptions, but this is rare except for
+        # assertRaises() and other similar functions, where the exception is
+        # raised by the last statement in the body, which usuaully isn't a
+        # problem.
         stmt.body.accept(self)
-        self.leave_with_or_try()
 
     def visit_import(self, imp: Import) -> None:
         for id, as_id in imp.ids:
@@ -283,10 +287,10 @@ def enter_block(self) -> None:
     def leave_block(self) -> None:
         self.blocks.pop()
 
-    def enter_with_or_try(self) -> None:
+    def enter_try(self) -> None:
         self.disallow_redef_depth += 1
 
-    def leave_with_or_try(self) -> None:
+    def leave_try(self) -> None:
         self.disallow_redef_depth -= 1
 
     def enter_loop(self) -> None:


Test patch:
diff --git a/test-data/unit/check-redefine.test b/test-data/unit/check-redefine.test
--- a/test-data/unit/check-redefine.test
+++ b/test-data/unit/check-redefine.test
@@ -104,16 +104,15 @@ def f() -> None:
 
 def g(): pass
 
-[case testCannotRedefineLocalWithinWith]
+[case testRedefineLocalWithinWith]
 # flags: --allow-redefinition
 def f() -> None:
     with g():
         x = 0
         x
-        g()  # Might raise an exception
-        x = '' \
-        # E: Incompatible types in assignment (expression has type "str", variable has type "int")
-    reveal_type(x) # E: Revealed type is 'builtins.int'
+        g()  # Might raise an exception, but we ignore this
+        x = ''
+    reveal_type(x) # E: Revealed type is 'builtins.str'
     y = 0
     y
     y = ''