15

I'm a bit confused about modifying tuple members. The following doesn't work:

>>> thing = (['a'],)
>>> thing[0] = ['b']
TypeError: 'tuple' object does not support item assignment
>>> thing
(['a'],)

But this does work:

>>> thing[0][0] = 'b'
>>> thing
(['b'],)

Also works:

>>> thing[0].append('c')
>>> thing
(['b', 'c'],)

Doesn't work, and works (huh?!):

>>> thing[0] += 'd'
TypeError: 'tuple' object does not support item assignment
>>> thing
(['b', 'c', 'd'],)

Seemingly equivalent to previous, but works:

>>> e = thing[0]
>>> e += 'e'
>>> thing
(['b', 'c', 'd', 'e'],)

So what exactly are the rules of the game, when you can and can't modify something inside a tuple? It seems to be more like prohibition of using the assignment operator for tuple members, but the last two cases are confusing me.

wim
  • 338,267
  • 99
  • 616
  • 750
  • 3
    *Note:* This is now in the [Python FAQ](https://docs.python.org/3/faq/programming.html#why-does-a-tuple-i-item-raise-an-exception-when-the-addition-works) – wim Oct 25 '19 at 19:52

3 Answers3

17

You can always modify a mutable value inside a tuple. The puzzling behavior you see with

>>> thing[0] += 'd'

is caused by +=. The += operator does in-place addition but also an assignment — the in-place addition works just file, but the assignment fails since the tuple is immutable. Thinking of it like

>>> thing[0] = thing[0] + 'd'

explains this better. We can use the dis module from the standard library to look at the bytecode generated from both expressions. With += we get an INPLACE_ADD bytecode:

>>> def f(some_list):
...     some_list += ["foo"]
... 
>>> dis.dis(f)
  2           0 LOAD_FAST                0 (some_list)
              3 LOAD_CONST               1 ('foo')
              6 BUILD_LIST               1
              9 INPLACE_ADD         
             10 STORE_FAST               0 (some_list)
             13 LOAD_CONST               0 (None)
             16 RETURN_VALUE        

With + we get a BINARY_ADD:

>>> def g(some_list):
...     some_list = some_list + ["foo"]
>>> dis.dis(g)
  2           0 LOAD_FAST                0 (some_list)
              3 LOAD_CONST               1 ('foo')
              6 BUILD_LIST               1
              9 BINARY_ADD          
             10 STORE_FAST               0 (some_list)
             13 LOAD_CONST               0 (None)
             16 RETURN_VALUE        

Notice that we get a STORE_FAST in both places. This is the bytecode that fails when you try to store back into a tuple — the INPLACE_ADD that comes just before works fine.

This explains why the "Doesn't work, and works" case leaves the modified list behind: the tuple already has a reference to the list:

>>> id(thing[0])
3074072428L

The list is then modified by the INPLACE_ADD and the STORE_FAST fails:

>>> thing[0] += 'd'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

So the tuple still has a reference to the same list, but the list has been modified in-place:

>>> id(thing[0])
3074072428L
>>> thing[0] 
['b', 'c', 'd']
Martin Geisler
  • 72,968
  • 25
  • 171
  • 229
  • Why isn't `thing[0] += 'e'` equivalent to `thing[0] = thing[0] + ['e']`, which would construct the correct list then lose it when assignment fails? That is, if it simplifies down to `.extend('e')`, why the `tmp` at all? Why not just `thing[0].extend('e')`? – Chris Lutz Feb 07 '12 at 07:31
  • @ChrisLutz: yeah, good point. You're completely right: it's not equivalent to `thing[0].extend('e')` but to `thing[0] = thing[0] + 'e'` like you suggest. I've fixed the answer to say that. – Martin Geisler Feb 07 '12 at 08:27
  • 1
    I still don't understand case I've labelled "Doesn't work, and works". I would expect it to either a) work, with the list modified or b) raise exception and leaving the list unmodified. How does it fail with exception AND modify the value? – wim Feb 08 '12 at 04:11
  • 1
    @wim: I've updated the answer to emphasise what goes wrong: first `INPLACE_ADD` is executed (this mutates the list) and then `STORE_FAST` is executed (this fails when you try to assign to a tuple). – Martin Geisler Feb 09 '12 at 08:40
  • 1
    "Thinking of it like `thing[0] = thing[0] + 'd'` explains this better" - except that with that code, neither the tuple nor the list would change (the addition would create a new list, which then could not be assigned into the tuple). What actually happens is more like `thing[0].extend('d')` followed by `thing[0] = thing[0]` (which fails even though it "shouldn't" have any effect). – Karl Knechtel Mar 14 '23 at 18:35
6

You can't modify the tuple, but you can modify the contents of things contained within the tuple. Lists (along with sets, dicts, and objects) are a reference type and thus the "thing" in the tuple is just a reference - the actual list is a mutable object which is pointed to by that reference and can be modified without changing the reference itself.

( + ,)       <--- your tuple (this can't be changed)
  |
  |
  v
 ['a']       <--- the list object your tuple references (this can be changed)

After thing[0][0] = 'b':

( + ,)       <--- notice how the contents of this are still the same
  |
  |
  v
 ['b']       <--- but the contents of this have changed

After thing[0].append('c'):

( + ,)       <--- notice how this is still the same
  |
  |
  v
 ['b','c']   <--- but this has changed again

The reason why += errors is that it's not completely equivalent to .append() - it actually does an addition and then an assignment (and the assignment fails), rather than merely appending in-place.

wjandrea
  • 28,235
  • 9
  • 60
  • 81
Amber
  • 507,862
  • 82
  • 626
  • 550
1

You cannot replace an element of a tuple, but you can replace the entire contents of the element. This will work:

thing[0][:] = ['b']
Lawrence D'Oliveiro
  • 2,768
  • 1
  • 15
  • 13