Why is it valid to assign to an empty list but not to an empty tuple?

Question:

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?

Asked By: j0ker

||

Answers:

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.

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.

Answered By: Blckknght

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.

Answered By: user774340

“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.

Answered By: flying sheep
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.