Repository: python/mypy

Issue:
Augmented assignment leads to type complaints
Hi,

I am using mypy 0.4.4. It seems augmented assignments aren't yet dealt with correctly.

```
weight0 = 65.5
weight0 = 65
weight0 *= 0.5                  # Gives error, unsup operand types for * ("int" and "float")
```

```
weight1 = 65.5
weight1 = 65
weight1 = weight1 * 0.5    # Gives no error
```



Discussion:
Wow. Good catch. Seems there's something strange going on indeed! We'll
investigate.

The error is not directly related to augmented assignment, I think. Mypy assumes that the second assignment kills the first

```
x = 65.5  # type: float
reveal_type(x)  # E: Revealed type is 'builtins.float'
x = 65
reveal_type(x)  # E: Revealed type is 'builtins.int'
```

Just like any other case of compatible assignment. So how is that different from the simpler case

```
x = 65
x += 5.0  # E: Unsupported operand types for + ("int" and "float")
```

Allowing the latter (inferring float without any other hint) can easily miss real type errors.

@elazarg:

If that's the cause, why then is

```
weight1 = 65.5
weight1 = 65
weight1 = weight1 * 0.5    # Gives no error
```

considered correct?

I think that because `x = x * 0.5` is an assignment, mypy considers it as introducing a "new variable", whereas `x *= 0.5` is a method call, and it operates on a previously defined variable.

In other words,  `x = x * 0.5` contains two distinct variables with possibly distinct types, and `x *= 0.5` contains a single variable.

It seems that `*=` doesn't rebind the variable type, unlike `=`. However, I think that it should.

`=` doesn't actually introduce new variable but it does introduce a new "type binding" for a variable. This means that the new binding must be compatible with the declared or inferred type of the variable -- mypy doesn't currently let you arbitrarily redefine a variable.

Indeed from a 'user' point of view, the disparity between simple assignment and augmented assignment is hard to comprehend. At least for me it was... So if it could be avoided I think it would be an improvement.


Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -1939,14 +1939,18 @@ def visit_operator_assignment_stmt(self,
         """Type check an operator assignment statement, e.g. x += 1."""
         lvalue_type = self.expr_checker.accept(s.lvalue)
         inplace, method = infer_operator_assignment_method(lvalue_type, s.op)
-        rvalue_type, method_type = self.expr_checker.check_op(
-            method, lvalue_type, s.rvalue, s)
-
-        if isinstance(s.lvalue, IndexExpr) and not inplace:
-            self.check_indexed_assignment(s.lvalue, s.rvalue, s.rvalue)
-        else:
+        if inplace:
+            # There is __ifoo__, treat as x = x.__ifoo__(y)
+            rvalue_type, method_type = self.expr_checker.check_op(
+                method, lvalue_type, s.rvalue, s)
             if not is_subtype(rvalue_type, lvalue_type):
                 self.msg.incompatible_operator_assignment(s.op, s)
+        else:
+            # There is no __ifoo__, treat as x = x <foo> y
+            expr = OpExpr(s.op, s.lvalue, s.rvalue)
+            expr.set_line(s)
+            self.check_assignment(lvalue=s.lvalue, rvalue=expr,
+                                  infer_lvalue_type=True, new_syntax=False)
 
     def visit_assert_stmt(self, s: AssertStmt) -> None:
         self.expr_checker.accept(s.expr)


Test patch:
diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test
--- a/test-data/unit/check-generics.test
+++ b/test-data/unit/check-generics.test
@@ -309,7 +309,6 @@ class C:
     def __add__(self, o: 'C') -> 'C': pass
 [out]
 main:7: error: Unsupported operand types for + ("C" and "B")
-main:7: error: Incompatible types in assignment (expression has type "B", target has type "C")
 main:8: error: Invalid index type "C" for A[C]; expected type "B"
 
 [case testOperatorAssignmentWithIndexLvalue2]
diff --git a/test-data/unit/check-statements.test b/test-data/unit/check-statements.test
--- a/test-data/unit/check-statements.test
+++ b/test-data/unit/check-statements.test
@@ -226,7 +226,7 @@ class B:
 class C: pass
 [out]
 main:3: error: Unsupported operand types for + ("A" and "B")
-main:4: error: Result type of + incompatible in assignment
+main:4: error: Incompatible types in assignment (expression has type "C", variable has type "B")
 main:5: error: Unsupported left operand type for + ("C")
 
 [case testMinusAssign]
@@ -246,7 +246,7 @@ class B:
 class C: pass
 [out]
 main:3: error: Unsupported operand types for - ("A" and "B")
-main:4: error: Result type of - incompatible in assignment
+main:4: error: Incompatible types in assignment (expression has type "C", variable has type "B")
 main:5: error: Unsupported left operand type for - ("C")
 
 [case testMulAssign]
@@ -1514,3 +1514,46 @@ class A(): pass
 class B(): pass
 [out]
 main:8: error: Incompatible types in assignment (expression has type "A", variable has type "B")
+
+[case testAugmentedAssignmentIntFloat]
+weight0 = 65.5
+reveal_type(weight0)  # E: Revealed type is 'builtins.float'
+weight0 = 65
+reveal_type(weight0)  # E: Revealed type is 'builtins.int'
+weight0 *= 'a'  # E: Incompatible types in assignment (expression has type "str", variable has type "float")
+weight0 *= 0.5
+reveal_type(weight0)  # E: Revealed type is 'builtins.float'
+weight0 *= object()  # E: Unsupported operand types for * ("float" and "object")
+reveal_type(weight0) # E: Revealed type is 'builtins.float'
+
+[builtins fixtures/float.pyi]
+
+[case testAugmentedAssignmentIntFloatMember]
+class A:
+    def __init__(self) -> None:
+        self.weight0 = 65.5
+        reveal_type(self.weight0)  # E: Revealed type is 'builtins.float'
+        self.weight0 = 65
+        reveal_type(self.weight0)  # E: Revealed type is 'builtins.int'
+        self.weight0 *= 'a'  # E: Incompatible types in assignment (expression has type "str", variable has type "float")
+        self.weight0 *= 0.5
+        reveal_type(self.weight0)  # E: Revealed type is 'builtins.float'
+        self.weight0 *= object()  # E: Unsupported operand types for * ("float" and "object")
+        reveal_type(self.weight0) # E: Revealed type is 'builtins.float'
+
+[builtins fixtures/float.pyi]
+
+[case testAugmentedAssignmentIntFloatDict]
+from typing import Dict
+d = {'weight0': 65.5}
+reveal_type(d['weight0'])  # E: Revealed type is 'builtins.float*'
+d['weight0'] = 65
+reveal_type(d['weight0'])  # E: Revealed type is 'builtins.float*'
+d['weight0'] *= 'a'  # E: Unsupported operand types for * ("float" and "str") # E: Incompatible types in assignment (expression has type "str", target has type "float")
+d['weight0'] *= 0.5
+reveal_type(d['weight0'])  # E: Revealed type is 'builtins.float*'
+d['weight0'] *= object()  # E: Unsupported operand types for * ("float" and "object")
+reveal_type(d['weight0']) # E: Revealed type is 'builtins.float*'
+
+[builtins fixtures/floatdict.pyi]
+