You can always modify a mutable value inside a tuple. The puzzling behavior you see with
>>> thing += 'd'
is caused by
+= 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 = thing + '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
>>> 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
+ we get a
>>> 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) 3074072428L
The list is then modified by the
INPLACE_ADD and the
>>> thing += '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) 3074072428L >>> thing ['b', 'c', 'd']
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)
thing = 'b':
( + ,) <--- notice how the contents of this are still the same | | v ['b'] <--- but the contents of this have changed
( + ,) <--- 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.