Repository: python/mypy

Issue:
Type error due to repeated use of underscore as dummy assignment target
Consider the following code:

``` python
from typing import Tuple

class A: pass
class B: pass
class C: pass

def f() -> Tuple[A, B, C]:
    pass

a = A()
a, _, _ = f()
```

mypy reports: `line 11: Incompatible types in assignment (expression has type "C", variable has type "B")`

Seems like mypy treats `_` as just a variable. That prevents it from being used as a dummy in assignments like the one above.

A solution might be to always give `_` the Any type.



Discussion:
Technically, Python itself treats it as a variable:

```
ryan@DevPC-LX:~$ python3
Python 3.3.5 |Continuum Analytics, Inc.| (default, Mar 10 2014, 11:19:31) 
[GCC 4.1.2 20080704 (Red Hat 4.1.2-54)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def a(_, _): pass
... 
  File "<stdin>", line 1
SyntaxError: duplicate argument '_' in function definition
>>> 1
1
>>> _
1
>>> 
```

See [here](https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers).

Hmm, this is a bit tricky. A conservative (but ad-hoc) option would be to give variables named `_` the type `Any` if and only if they are only assigned to but never read. Actually, we could give any local variable that is only assigned to in a multiple assignment but that is never read the type `Any`, I think.

So this would be accepted, since `dummy` is never read:

```
def foo() -> None:
    foobar, dummy, dummy = 1, 2, 'x'
    g(foobar)
```

Not sure if this generalization would be that useful, though.

We could also give a warning if a local variable is only assigned to, unless this happens in a multiple assignment (or a similar context) where it's okay to ignore the values of all variables except at least one.

I argue that only names that start with an underscore should be considered.

I have some code like `a, _b, _c = foo()` where I remove the underscore if I'm actually going to use the variable but keep a name attached for documentation reasons.

Further complication: what about people who use `_` for `gettext` ? IMO that is unique enough that it's worth special-casing.

But the rule for ignoring only things are only assigned to would work with `gettext` / `_`, as then you'd actually read the value of `_`. We probably want to give a warning if there's a normal-looking local variable such as `dummy` that is never read.

(Raised priority to high since this request keeps coming.)

Resolving patch:
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -1826,6 +1826,10 @@ def analyze_lvalue(self, lval: Lvalue, nested: bool = False,
                 lval.kind = LDEF
                 lval.fullname = lval.name
                 self.add_local(v, lval)
+                if lval.name == '_':
+                    # Special case for assignment to local named '_': always infer 'Any'.
+                    typ = AnyType(TypeOfAny.special_form)
+                    self.store_declared_types(lval, typ)
             elif not self.is_func_scope() and (self.type and
                                                lval.name not in self.type.names):
                 # Define a new attribute within class body.
diff --git a/mypy/types.py b/mypy/types.py
--- a/mypy/types.py
+++ b/mypy/types.py
@@ -281,6 +281,7 @@ class TypeOfAny(Enum):
     from_error = 'from_error'
     # Is this a type that can't be represented in mypy's type system? For instance, type of
     # call to NewType...). Even though these types aren't real Anys, we treat them as such.
+    # Also used for variables named '_'.
     special_form = 'special_form'
     # Does this Any come from interaction with another Any?
     from_another_any = 'from_another_any'


Test patch:
diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test
--- a/test-data/unit/check-inference.test
+++ b/test-data/unit/check-inference.test
@@ -2303,3 +2303,118 @@ class C:
     def f(self, x) -> None:
         # TODO: It would be better for the type to be Any here
         self.a.y  # E: "None" has no attribute "y"
+
+-- Special case for assignment to '_'
+-- ----------------------------------
+
+[case testUnusedTargetLocal]
+def foo() -> None:
+    _ = 0
+    _ = ''
+
+[case testUnusedTargetNotGlobal]
+_ = 0
+_ = ''  # E: Incompatible types in assignment (expression has type "str", variable has type "int")
+
+[case testUnusedTargetNotClass]
+class C:
+    _ = 0
+    _ = ''  # E: Incompatible types in assignment (expression has type "str", variable has type "int")
+
+[case testUnusedTargetTupleUnpacking]
+def foo() -> None:
+    _, _ = (0, '')
+    _ = 0
+    _ = ''
+def bar() -> None:
+    t = (0, '')
+    _, _ = t
+    _ = 0
+    _ = ''
+
+[case testUnusedTargetMultipleTargets]
+def foo() -> None:
+    _ = x = 0
+    _ = y = ''
+    _ = 0
+    _ = ''
+def bar() -> None:
+    x = _ = 0
+    y = _ = ''
+    _ = 0
+    _ = ''
+    x + 0
+    y + ''
+    x + ''  # E: Unsupported operand types for + ("int" and "str")
+    y + 0  # E: Unsupported operand types for + ("str" and "int")
+
+[case testUnusedTargetNotImport]
+import d, c, b, a
+[file _.py]
+def f(): pass
+[file m.py]
+def f(): pass
+_ = f
+_ = 0  # E: Incompatible types in assignment (expression has type "int", variable has type "Callable[[], Any]")
+[file a.py]
+def foo() -> None:
+    import _
+    _.f()
+    _ = 0  # E: Incompatible types in assignment (expression has type "int", variable has type Module)
+[file b.py]
+def foo() -> None:
+    import m as _
+    _.f()
+    _ = 0  # E: Incompatible types in assignment (expression has type "int", variable has type Module)
+[file c.py]
+def foo() -> None:
+    from m import _
+    _()
+    _ = 0  # E: Incompatible types in assignment (expression has type "int", variable has type "Callable[[], Any]")
+[file d.py]
+def foo() -> None:
+    from m import f as _
+    _()
+    _ = 0  # E: Incompatible types in assignment (expression has type "int", variable has type "Callable[[], Any]")
+[builtins fixtures/module.pyi]
+
+[case testUnusedTargetNotClass]
+def foo() -> None:
+    class _:
+        pass
+    _().method()  # E: "_" has no attribute "method"
+
+[case testUnusedTargetNotDef]
+def foo() -> None:
+    def _() -> int:
+        pass
+    _() + ''  # E: Unsupported operand types for + ("int" and "str")
+
+[case testUnusedTargetForLoop]
+def f() -> None:
+    a = [(0, '', 0)]
+    for _, _, x in a:
+        x = 0
+        x = ''  # E: Incompatible types in assignment (expression has type "str", variable has type "int")
+    _ = 0
+    _ = ''
+[builtins fixtures/list.pyi]
+
+[case testUnusedTargetWithClause]
+class C:
+    def __enter__(self) -> int: pass
+    def __exit__(self, *args): pass
+def f() -> None:
+    with C() as _: pass
+    _ = 0
+    _ = ''
+
+[case testUnusedTargetNotExceptClause]
+# Things don't work for except clauses.
+# This is due to the implementation, but it's just as well.
+def f() -> None:
+    try: pass
+    except BaseException as _:
+        _ = 0  # E: Incompatible types in assignment (expression has type "int", variable has type "BaseException")
+        _ = ''  # E: Incompatible types in assignment (expression has type "str", variable has type "BaseException")
+[builtins fixtures/exception.pyi]