Repository: python/mypy

Issue:
SelfType or another way to spell "type of self" (or, How to define a copy() function)
A colleague and I were wondering how to define a copy() method in a base class so that when called on an instance of a subclass it is known that it returns an instance of that subclass. We found the following solution:

```
T = TypeVar('T')
class Copyable:
    def copy(self: T) -> T:
        return self.__class__()  # type: ignore
class X(Copyable):
    def foo(self):
        pass
x = X()
x.foo()
xx = x.copy()
xx.foo()  # OK
```

Note that the `# type: ignore` is required for two reasons: you can't call `self.__class__()`, _and_ it doesn't like the return type. Try e.g. `return self` and you get

```
x.py:5: error: Incompatible return value type: expected T`-1, got x.Copyable
```

It works in `--py2` mode too:

```
    def copy(self):
        # type: (T) -> T
        return self.__class__()  # type: ignore
```

**UPDATE: This doesn't actually work. :-( See next comment.**



Discussion:
Hmm if this works it's not by design. I actually get errors when type checking the example:

```
t.py:11: error: Need type annotation for variable   (on line xx = x.copy())
t.py:12: error: None has no attribute "foo"
```

To make this work properly we could provide a new kind of type: a "self" type, which is basically the concrete type of `self` (in a derived class it would refer to the derived class, even if the definition is inherited). It's been used in a bunch of languages, though I'm not sure if any mainstream language has it. To make it work well we'd also need to support calling `self.__class__`. 

We could have something like this:

```
from typing import SelfType

class Copyable:
    def copy(self) -> SelfType:
        return self.__class__()  # TODO still need to be able to call self.__class__
class X(Copyable):
    def foo(self):
        pass
x = X()
x.foo()
xx = x.copy()
xx.foo()  # OK
```

`SelfType` would probably only be usable in a covariant position such as in a return type. `self` and `self.__class__(...)` would have the type `SelfType`.

Oh, I could have sworn that it worked but it was late. :-( So yes, I guess this is two feature requests: `SelfType` and calling `self.__class__`.

(It seems to work when the class is generic, but who knows what other effects that has.)


Resolving patch:
diff --git a/mypy/parse.py b/mypy/parse.py
--- a/mypy/parse.py
+++ b/mypy/parse.py
@@ -631,9 +631,10 @@ def parse_arg_list(self, allow_signature: bool = True,
                 elif self.current_str() == '(':
                     arg, extra_stmt, names = self.parse_tuple_arg(len(args))
                     args.append(arg)
-                    extra_stmts.append(extra_stmt)
+                    if extra_stmt is not None:
+                        extra_stmts.append(extra_stmt)
+                        has_tuple_arg = True
                     arg_names.extend(names)
-                    has_tuple_arg = True
                 else:
                     arg, require_named = self.parse_normal_arg(
                         require_named,
@@ -691,6 +692,10 @@ def parse_tuple_arg(self, index: int) -> Tuple[Argument, AssignmentStmt, List[st
         function body (the second return value).
 
         Return tuple (argument, decomposing assignment, list of names defined).
+
+        Special case: if the argument is just (x) then it's not a tuple;
+        we indicate this by returning (argument, None, ['x']).
+        However, if the argument is (x,) then it *is* a (singleton) tuple.
         """
         line = self.current().line
         # Generate a new argument name that is very unlikely to clash with anything.
@@ -700,13 +705,14 @@ def parse_tuple_arg(self, index: int) -> Tuple[Argument, AssignmentStmt, List[st
         paren_arg = self.parse_parentheses()
         self.verify_tuple_arg(paren_arg)
         if isinstance(paren_arg, NameExpr):
-            # This isn't a tuple. Revert to a normal argument. We'll still get a no-op
-            # assignment below but that's benign.
+            # This isn't a tuple. Revert to a normal argument.
             arg_name = paren_arg.name
-        rvalue = NameExpr(arg_name)
-        rvalue.set_line(line)
-        decompose = AssignmentStmt([paren_arg], rvalue)
-        decompose.set_line(line)
+            decompose = None
+        else:
+            rvalue = NameExpr(arg_name)
+            rvalue.set_line(line)
+            decompose = AssignmentStmt([paren_arg], rvalue)
+            decompose.set_line(line)
         kind = nodes.ARG_POS
         initializer = None
         if self.current_str() == '=':


Test patch:
diff --git a/mypy/test/data/check-python2.test b/mypy/test/data/check-python2.test
--- a/mypy/test/data/check-python2.test
+++ b/mypy/test/data/check-python2.test
@@ -174,5 +174,22 @@ main: note: In function "f":
 main:1: error: Parse error before ): Ellipses cannot accompany other argument types in function type signature.
 
 [case testLambdaTupleArgInPython2]
-lambda (x, y): x + y
+f = lambda (x, y): x + y
+f((0, 0))
+[out]
+
+[case testLambdaSingletonTupleArgInPython2]
+f = lambda (x,): x + 1
+f((0,))
+[out]
+
+[case testLambdaNoTupleArgInPython2]
+f = lambda (x): x + 1
+f(0)
+[out]
+
+[case testDefTupleEdgeCasesPython2]
+def f((x,)): return x
+def g((x)): return x
+f(0) + g(0)
 [out]
diff --git a/mypy/test/data/parse-python2.test b/mypy/test/data/parse-python2.test
--- a/mypy/test/data/parse-python2.test
+++ b/mypy/test/data/parse-python2.test
@@ -302,6 +302,34 @@ MypyFile:1(
         ReturnStmt:1(
           NameExpr(z))))))
 
+[case testLambdaSingletonTupleArgListInPython2]
+lambda (x,): z
+[out]
+MypyFile:1(
+  ExpressionStmt:1(
+    FuncExpr:1(
+      Args(
+        Var(__tuple_arg_1))
+      Block:1(
+        AssignmentStmt:1(
+          TupleExpr:1(
+            NameExpr(x))
+          NameExpr(__tuple_arg_1))
+        ReturnStmt:1(
+          NameExpr(z))))))
+
+[case testLambdaNoTupleArgListInPython2]
+lambda (x): z
+[out]
+MypyFile:1(
+  ExpressionStmt:1(
+    FuncExpr:1(
+      Args(
+        Var(x))
+      Block:1(
+        ReturnStmt:1(
+          NameExpr(z))))))
+
 [case testInvalidExprInTupleArgListInPython2_1]
 def f(x, ()): pass
 [out]
@@ -343,9 +371,6 @@ MypyFile:1(
       Var(x)
       Var(y))
     Block:1(
-      AssignmentStmt:1(
-        NameExpr(y)
-        NameExpr(y))
       PassStmt:1())))
 
 [case testDuplicateNameInTupleArgList_python2]