Repository: python/mypy

Issue:
Mypy should infer result of `x in y` as a `bool`
Python always casts the value of an `in` test to `bool`, so `a in b` will be a `bool` regardless of what the return type of `b.__contains__` is. Mypy does not internalize this idea though. So for example, the following code:
```python
class MyClass:
    def __contains__(self, x: object) -> int:
        return 42

    def my_func(self) -> bool:
        return 1 in self
```
Generates this error from Mypy:
```
test.py:6: error: Incompatible return value type (got "int", expected "bool")
```
Though actually, this should be fine, as even though `MyClass.__contains__` returns `42`, `1 in self` will evaluate to `True`. This is an artificial example, though I came across this error when using a container type that Mypy did not have the import file for, and so assumed a return value of `Any` for my `in` test. This generated a warning when run with `--strict`.


Discussion:
Interesting. Confirmed this is the case. Note that `__contains__` is unique here -- overriding e.g. `__lt__` as returning `int` does not cast the return value to `bool`. (Probably because of "rich comparisons".)

Resolving patch:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -1853,6 +1853,10 @@ def visit_ellipsis(self, e: EllipsisExpr) -> Type:
 
     def visit_op_expr(self, e: OpExpr) -> Type:
         """Type check a binary operator expression."""
+        if e.op == 'in':
+            self.accept(e.right)
+            self.accept(e.left)
+            return self.bool_type()
         if e.op == 'and' or e.op == 'or':
             return self.check_boolean_op(e, e)
         if e.op == '*' and isinstance(e.left, ListExpr):
@@ -1885,7 +1889,8 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type:
         Comparison expressions are type checked consecutive-pair-wise
         That is, 'a < b > c == d' is check as 'a < b and b > c and c == d'
         """
-        result = None  # type: Optional[Type]
+        result = None      # type: Optional[Type]
+        sub_result = None  # type: Optional[Type]
 
         # Check each consecutive operand pair and their operator
         for left, right, operator in zip(e.operands, e.operands[1:], e.operators):
@@ -1900,8 +1905,9 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type:
                 # are just to verify whether something is valid typing wise).
                 local_errors = self.msg.copy()
                 local_errors.disable_count = 0
-                sub_result, method_type = self.check_method_call_by_name(
+                _, method_type = self.check_method_call_by_name(
                     '__contains__', right_type, [left], [ARG_POS], e, local_errors)
+                sub_result = self.bool_type()
                 if isinstance(right_type, PartialType):
                     # We don't really know if this is an error or not, so just shut up.
                     pass
@@ -1915,13 +1921,10 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type:
                         [None],
                         self.bool_type(),
                         self.named_type('builtins.function'))
-                    sub_result = self.bool_type()
                     if not is_subtype(left_type, itertype):
                         self.msg.unsupported_operand_types('in', left_type, right_type, e)
                 else:
                     self.msg.add_errors(local_errors)
-                if operator == 'not in':
-                    sub_result = self.bool_type()
             elif operator in nodes.op_methods:
                 method = self.get_operator_method(operator)
                 sub_result, method_type = self.check_op(method, left_type, right, e,


Test patch:
diff --git a/test-data/unit/check-dynamic-typing.test b/test-data/unit/check-dynamic-typing.test
--- a/test-data/unit/check-dynamic-typing.test
+++ b/test-data/unit/check-dynamic-typing.test
@@ -154,7 +154,7 @@ n = 0
 a and d
 a or d
 if int():
-    c = a in d
+    c = a in d  # E: Incompatible types in assignment (expression has type "bool", variable has type "C")
 if int():
     c = b and d # E: Incompatible types in assignment (expression has type "Union[bool, Any]", variable has type "C")
 if int():
diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test
--- a/test-data/unit/check-expressions.test
+++ b/test-data/unit/check-expressions.test
@@ -416,16 +416,24 @@ class D(Iterable[A]):
 [builtins fixtures/bool.pyi]
 
 [case testNonBooleanContainsReturnValue]
-a, b = None, None # type: (A, bool)
+a, b, c = None, None, None # type: (A, bool, int)
 if int():
     b = a not in a
 if int():
-    b = a in a # E: Incompatible types in assignment (expression has type "object", variable has type "bool")
+    b = a in a
+if int():
+    c = a not in a  # E: Incompatible types in assignment (expression has type "bool", variable has type "int")
+if int():
+    c = a in a  # E: Incompatible types in assignment (expression has type "bool", variable has type "int")
 
 class A:
-  def __contains__(self, x: 'A') -> object: pass
+  def __contains__(self, x: 'A') -> int: pass
 [builtins fixtures/bool.pyi]
 
+[case testInWithInvalidArgs]
+a = 1 in ([1] + ['x'])  # E: List item 0 has incompatible type "str"; expected "int"
+[builtins fixtures/list.pyi]
+
 [case testEq]
 
 a, b = None, None # type: (A, bool)
diff --git a/test-data/unit/deps-expressions.test b/test-data/unit/deps-expressions.test
--- a/test-data/unit/deps-expressions.test
+++ b/test-data/unit/deps-expressions.test
@@ -303,8 +303,9 @@ class A:
     def __contains__(self, x: B) -> int: pass
 class B: pass
 
-def f() -> int:
+def f() -> bool:
     return B() in A()
+[builtins fixtures/bool.pyi]
 [out]
 <m.A.__contains__> -> m.f
 <m.A.__init__> -> m.f