Repository: python/mypy

Issue:
__iadd__ doesn't work when going through a Dict or List
```

class A(object):
    def __init__(self):
        self.a = 0

    def __iadd__(self, a):
        # type: (int) -> A
        self.a += 1
        return self

a = A()
a += 1

b = {0: a}
b[0] += 1

c = [a]
c[0] += 1

print(a.a)
```

Yields the following errors:

```
rt1.py:15: error: Incompatible types in assignment (expression has type "int", target has type "A")
rt1.py:18: error: No overload variant of "__setitem__" of "list" matches argument types [builtins.int, builtins.int]
```



Discussion:
Thanks for reporting this bug!

On a bit of debugging, here's the relevant logic:
https://github.com/python/mypy/blob/897398eff/mypy/checker.py#L1556

```
    def visit_operator_assignment_stmt(self,
                                       s: OperatorAssignmentStmt) -> Type:
        """Type check an operator assignment statement, e.g. x += 1."""
        lvalue_type = self.accept(s.lvalue)
        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):
            self.check_indexed_assignment(s.lvalue, s.rvalue, s.rvalue)
        else:
            if not is_subtype(rvalue_type, lvalue_type):
                self.msg.incompatible_operator_assignment(s.op, s)
```

I think the issue is that when `s.lvalue` is an `IndexExpr`, we forget the possibility that the lvalue actually has an in-place method for this operator and we attempt to check an outright assignment. (I'm not convinced it's quite right even when there isn't an in-place method -- that `check_indexed_assignment` line goes and type-checks it as if we're assigning the type of the actual RHS, rather than `rvalue_type` which is the type the operator returns -- but that'd be a separate issue.)

Will see if I can work up an easy fix.


Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -1557,11 +1557,11 @@ def visit_operator_assignment_stmt(self,
                                        s: OperatorAssignmentStmt) -> Type:
         """Type check an operator assignment statement, e.g. x += 1."""
         lvalue_type = self.accept(s.lvalue)
-        method = infer_operator_assignment_method(lvalue_type, s.op)
+        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):
+        if isinstance(s.lvalue, IndexExpr) and not inplace:
             self.check_indexed_assignment(s.lvalue, s.rvalue, s.rvalue)
         else:
             if not is_subtype(rvalue_type, lvalue_type):
@@ -2630,19 +2630,19 @@ def is_more_precise_signature(t: CallableType, s: CallableType) -> bool:
     return is_more_precise(t.ret_type, s.ret_type)
 
 
-def infer_operator_assignment_method(type: Type, operator: str) -> str:
-    """Return the method used for operator assignment for given value type.
+def infer_operator_assignment_method(type: Type, operator: str) -> Tuple[bool, str]:
+    """Determine if operator assignment on given value type is in-place, and the method name.
 
-    For example, if operator is '+', return '__iadd__' or '__add__' depending
-    on which method is supported by the type.
+    For example, if operator is '+', return (True, '__iadd__') or (False, '__add__')
+    depending on which method is supported by the type.
     """
     method = nodes.op_methods[operator]
     if isinstance(type, Instance):
         if operator in nodes.ops_with_inplace_method:
-            inplace = '__i' + method[2:]
-            if type.type.has_readable_member(inplace):
-                method = inplace
-    return method
+            inplace_method = '__i' + method[2:]
+            if type.type.has_readable_member(inplace_method):
+                return True, inplace_method
+    return False, method
 
 
 def is_valid_inferred_type(typ: Type) -> bool:


Test patch:
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
@@ -301,6 +301,22 @@ a *= ''
 a += '' # E: Argument 1 to "__iadd__" of "A" has incompatible type "str"; expected "int"
 a *= 1  # E: Argument 1 to "__imul__" of "A" has incompatible type "int"; expected "str"
 
+[case testInplaceSetitem]
+class A(object):
+    def __init__(self):
+        self.a = 0
+
+    def __iadd__(self, a):
+        # type: (int) -> A
+        self.a += 1
+        return self
+
+a = A()
+b = [a]
+b[0] += 1
+[builtins fixtures/list.pyi]
+[out]
+
 
 -- Assert statement
 -- ----------------