40

So I have this code:

tup = ([1,2,3],[7,8,9])
tup[0] += (4,5,6)

which generates this error:

TypeError: 'tuple' object does not support item assignment

While this code:

tup = ([1,2,3],[7,8,9])
try:
    tup[0] += (4,5,6)
except TypeError:
    print tup

prints this:

([1, 2, 3, 4, 5, 6], [7, 8, 9])

Is this behavior expected?

Note

I realize this is not a very common use case. However, while the error is expected, I did not expect the list change.

idanshmu
  • 5,061
  • 6
  • 46
  • 92
  • 3
    In fact your first attempt shows an error but works: `>>> tup[0]` returns `[1, 2, 3, 4, 5, 6]` to me. – fedorqui Apr 20 '15 at 11:58
  • Re your edit: it may not be a common use case, but it's extremely important that you understand the reasons for this. – Joe Apr 20 '15 at 12:06

4 Answers4

37

Yes it's expected.

A tuple cannot be changed. A tuple, like a list, is a structure that points to other objects. It doesn't care about what those objects are. They could be strings, numbers, tuples, lists, or other objects.

So doing anything to one of the objects contained in the tuple, including appending to that object if it's a list, isn't relevant to the semantics of the tuple.

(Imagine if you wrote a class that had methods on it that cause its internal state to change. You wouldn't expect it to be impossible to call those methods on an object based on where it's stored).

Or another example:

>>> l1 = [1, 2, 3]
>>> l2 = [4, 5, 6]
>>> t = (l1, l2)
>>> l3 = [l1, l2]
>>> l3[1].append(7)

Two mutable lists referenced by a list and by a tuple. Should I be able to do the last line (answer: yes). If you think the answer's no, why not? Should t change the semantics of l3 (answer: no).

If you want an immutable object of sequential structures, it should be tuples all the way down.

Why does it error?

This example uses the infix operator:

Many operations have an “in-place” version. The following functions provide a more primitive access to in-place operators than the usual syntax does; for example, the statement x += y is equivalent to x = operator.iadd(x, y). Another way to put it is to say that z = operator.iadd(x, y) is equivalent to the compound statement z = x; z += y.

https://docs.python.org/2/library/operator.html

So this:

l = [1, 2, 3]
tup = (l,)
tup[0] += (4,5,6)

is equivalent to this:

l = [1, 2, 3]
tup = (l,)
x = tup[0]
x = x.__iadd__([4, 5, 6]) # like extend, but returns x instead of None
tup[0] = x

The __iadd__ line succeeds, and modifies the first list. So the list has been changed. The __iadd__ call returns the mutated list.

The second line tries to assign the list back to the tuple, and this fails.

So, at the end of the program, the list has been extended but the second part of the += operation failed. For the specifics, see this question.

Community
  • 1
  • 1
Joe
  • 46,419
  • 33
  • 155
  • 245
12

Well I guess tup[0] += (4, 5, 6) is translated to:

tup[0] = tup[0].__iadd__((4,5,6))

tup[0].__iadd__((4,5,6)) is executed normally changing the list in the first element. But the assignment fails since tuples are immutables.

JuniorCompressor
  • 19,631
  • 4
  • 30
  • 57
  • 2
    This is absolutely correct. We can see that by using the `dis` module. Given `def f(tup):tup[0] += (4, 5, 6)`. If we use `dis.dis(f)`. Included in the bytecode is INPLACE_ADD and STORE_SUBSCR. INPLACE_ADD corresponds to `obj.__iadd__((4,5,6))`. This mutating step succeeds. STORE_SUBSCR is where the result is assigned to the tuple `tup[0] = result_of_idadd`. This fails because tuples cannot be modified. – Steven Rumbalski Apr 20 '15 at 15:14
6

Tuples cannot be changed directly, correct. Yet, you may change a tuple's element by reference. Like:

>>> tup = ([1,2,3],[7,8,9])
>>> l = tup[0]
>>> l += (4,5,6)
>>> tup
([1, 2, 3, 4, 5, 6], [7, 8, 9])
a5kin
  • 1,335
  • 16
  • 20
2

The Python developers wrote an official explanation about why it happens here: https://docs.python.org/2/faq/programming.html#why-does-a-tuple-i-item-raise-an-exception-when-the-addition-works

The short version is that += actually does two things, one right after the other:

  1. Run the thing on the right.
  2. assign the result to the variable on the left

In this case, step 1 works because you’re allowed to add stuff to lists (they’re mutable), but step 2 fails because you can’t put stuff into tuples after creating them (tuples are immutable).

In a real program, I would suggest you don't do a try-except clause, because tup[0].extend([4,5,6]) does the exact same thing.

Cheyn Shmuel
  • 428
  • 8
  • 15