Search code examples
pythonlisttuplesimmutabilitymutable

a mutable type inside an immutable container


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.


Solution

  • 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']