31

This came up in a recent PyCon talk.

The statement

[] = []

does nothing meaningful, but it does not throw an exception either. I have the feeling this must be due to unpacking rules. You can do tuple unpacking with lists too, e.g.,

[a, b] = [1, 2]

does what you would expect. As logical consequence, this also should work, when the number of elements to unpack is 0, which would explain why assigning to an empty list is valid. This theory is further supported by what happens when you try to assign a non-empty list to an empty list:

>>> [] = [1]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: too many values to unpack

I would be happy with this explanation, if the same would also be true for tuples. If we can unpack to a list with 0 elements, we should also be able to unpack to a tuple with 0 elements, no? However:

>>> () = ()
  File "<stdin>", line 1
SyntaxError: can't assign to ()

It seems like unpacking rules are not applied for tuples as they are for lists. I cannot think of any explanation for this inconsistency. Is there a reason for this behavior?

j0ker
  • 4,069
  • 4
  • 43
  • 65
  • 3
    @ozgur but unpacking with tuples does work: `a, b = 1, 2` is valid ... – Bas Swinckels Apr 25 '15 at 20:02
  • I am not sure but I think `[] = []` is not unpacking. However, I got suprised when I saw this would be doable: `[a, b] = [1, 2]`. Instead, I would do `a, b = (1, 2)` – Ozgur Vatansever Apr 25 '15 at 20:08
  • 4
    I get the feeling there isn't going to be any interesting principle at work here. The best answer will probably be something like "here's the section of the code generator where it checks the validity of the LHS of an assignment, and here's the check that catches `()` but lets `[]` through". Maybe it'll be because `()` is recognized as a constant or something. – user2357112 Apr 25 '15 at 20:10
  • @ozgur it's not about mutation - the same `[] = [1]` and `{} = {'a':1}` also raises error. it's maybe () acts like the method itself. beacuse `[].__add_([[]])` results `[[]]` but, `().__add__(((())))` just (). – marmeladze Apr 25 '15 at 20:12
  • 8
    There is a [python bug](http://bugs.python.org/issue23275) dating from january 2015 showing the cause in the Python sources. Note that both assignments should be illegal according to the documentation. The discussion seems to lean slightly towards allowing assignment to `()` – mkiever Apr 25 '15 at 20:35
  • Furthering @mkievers comment, new Python versions now allow assignment to both `[]` and `()`. – gerrit Nov 14 '17 at 13:13

4 Answers4

21

The comment by @user2357112 that this seems to be coincidence appears to be correct. The relevant part of the Python source code is in Python/ast.c:

switch (e->kind) {
    # several cases snipped
    case List_kind:
        e->v.List.ctx = ctx;
        s = e->v.List.elts;
        break;
    case Tuple_kind:
        if (asdl_seq_LEN(e->v.Tuple.elts))  {
            e->v.Tuple.ctx = ctx;
            s = e->v.Tuple.elts;
        }
        else {
            expr_name = "()";
        }
        break;
    # several more cases snipped
}
/* Check for error string set by switch */
if (expr_name) {
    char buf[300];
    PyOS_snprintf(buf, sizeof(buf),
                  "can't %s %s",
                  ctx == Store ? "assign to" : "delete",
                  expr_name);
    return ast_error(c, n, buf);
}

tuples have an explicit check that the length is not zero and raise an error when it is. lists do not have any such check, so there's no exception raised.

I don't see any particular reason for allowing assignment to an empty list when it is an error to assign to an empty tuple, but perhaps there's some special case that I'm not considering. I'd suggest that this is probably a (trivial) bug and that the behaviors should be the same for both types.

Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • It is perfectly legal in python 2.7 to a have a tuple of length 0. – clj Apr 25 '15 at 20:25
  • 6
    @clj: Not on the left side of an assignment it isn't. – user2357112 Apr 25 '15 at 20:26
  • 1
    I was just about to post a similar answer. I was pretty surprised when I found the code and saw that it was a specific check for empty tuples, rather than a more general check that happens to apply to empty tuples but not empty lists. – user2357112 Apr 25 '15 at 20:29
  • The bug is now fixed allowing for tuples in the right hand side too: https://bugs.python.org/issue23275 – Mr_and_Mrs_D Dec 14 '16 at 22:44
13

I decided to try to use dis to figure out what's going on here, when I tripped over something curious:

>>> def foo():
...   [] = []
... 
>>> dis.dis(foo)
  2           0 BUILD_LIST               0
              3 UNPACK_SEQUENCE          0
              6 LOAD_CONST               0 (None)
              9 RETURN_VALUE        
>>> def bar():
...   () = ()
... 
  File "<stdin>", line 2
SyntaxError: can't assign to ()

Somehow the Python compiler special-cases an empty tuple on the LHS. This difference varies from the specification, which states:

Assignment of an object to a single target is recursively defined as follows.

...

  • If the target is a target list enclosed in parentheses or in square brackets: The object must be an iterable with the same number of items as there are targets in the target list, and its items are assigned, from left to right, to the corresponding targets.

So it looks like you've found a legitimate, although ultimately inconsequential, bug in CPython (2.7.8 and 3.4.1 tested).

IronPython 2.6.1 exhibits the same difference, but Jython 2.7b3+ has a stranger behavior, with () = () starting a statement with seemingly no way to end it.

Community
  • 1
  • 1
Ignacio Vazquez-Abrams
  • 776,304
  • 153
  • 1,341
  • 1,358
  • 2
    [According to the spec](https://docs.python.org/3/reference/simple_stmts.html#assignment-statements), it should reject both `[] = []` and `() = []`. – jfs Apr 25 '15 at 20:48
  • I accepted @Blckknght's answer because it accurately answers my question, but this one is great too! Didn't know about ``dis`` before. – j0ker Apr 25 '15 at 20:48
4

It's a bug.

http://bugs.python.org/issue23275

However, it seems to be harmless so I doubt it would get fixed for fear of breaking working code.

3

“Assigning to a list” is the wrong way to think about it.

In all cases you are unpacking: The Python interpreter creates an unpacking instruction from all three ways to write it, there are no lists or tuples involved on the left hand side (code courtesy of /u/old-man-prismo):

>>> def f():
...     iterable = [1, 2]
...     a, b = iterable
...     (c, d) = iterable
...     [e, f] = iterable
...
>>> from dis import dis
>>> dis(f)
  2           0 LOAD_CONST               1 (1)
              3 LOAD_CONST               2 (2)
              6 BUILD_LIST               2
              9 STORE_FAST               0 (iterable)

  3          12 LOAD_FAST                0 (iterable)
             15 UNPACK_SEQUENCE          2
             18 STORE_FAST               1 (a)
             21 STORE_FAST               2 (b)

  4          24 LOAD_FAST                0 (iterable)
             27 UNPACK_SEQUENCE          2
             30 STORE_FAST               3 (c)
             33 STORE_FAST               4 (d)

  5          36 LOAD_FAST                0 (iterable)
             39 UNPACK_SEQUENCE          2
             42 STORE_FAST               5 (e)
             45 STORE_FAST               6 (f)
             48 LOAD_CONST               0 (None)
             51 RETURN_VALUE      

As you can see, all three statements are exactly the same.

What unpacking does now is basically:

_iterator = iter(some_iterable)
a = next(_iterator)
b = next(_iterator)
for superfluous_element in _iterator:
    # this only happens if there’s something left
    raise SyntaxError('Expected some_iterable to have 2 elements')

Analoguously for more or less names on the left side.

Now as @blckknght said: The compiler for some reason checks if the left hand side is an empty tuple and disallows that, but not if it’s an empty list.

It’s only consistent and logical to allow assigning to 0 names: Why not? You basically just assert that the iterable on the right hand side is empty. That opinion also seems to emerge as consensus in the bug report @gecko mentioned: Let’s allow () = iterable.

flying sheep
  • 8,475
  • 5
  • 56
  • 73