Python Part I · Module 6 · Tuples
Tuples Cannot Be Changed
Chapter 6.1 said a tuple cannot be changed and showed one error as evidence. This chapter is the full account, and it exists because the short version of the rule is not quite true. A tuple genuinely cannot be modified: you cannot replace an element, add one, remove one or reorder them, and Python refuses each attempt with a message worth being able to read. But that guarantee is about the tuple, not about everything inside it, and a tuple holding a list behaves in a way that surprises people who learned the short version. Getting this exactly right is what turns immutability from a restriction you work around into the property that makes a tuple useful where a list is not allowed.
Foundation15 min readPythonPart IFundamentalsTuplesImmutability
Part I · Module 6 · Chapter 6.4 · Tuples Cannot Be Changed
1. Every Way of Changing One Is Refused
Chapter 5.3 gave you a toolkit for changing a list. Every item in it fails on a tuple, and the failures come in two distinct shapes.
Replacing an element:
p = (10, 20)
p[0] = 99TypeError: 'tuple' object does not support item assignmentRemoving an element by position:
p = (10, 20)
del p[0]TypeError: 'tuple' object doesn't support item deletionBoth are TypeError, and both say the same thing in different words: this kind of value has no such operation. The syntax is fine; the operation does not exist for tuples.
Now the other shape:
p = (10, 20)
p.append(30)AttributeError: 'tuple' object has no attribute 'append'p = (10, 20)
p.remove(10)AttributeError: 'tuple' object has no attribute 'remove'p = (2, 1)
p.sort()AttributeError: 'tuple' object has no attribute 'sort'AttributeError rather than TypeError, and the difference is informative. append, remove and sort are not operations that tuples refuse — they are operations tuples do not have. Chapter 4.6 met this error when "abc".len() failed: you asked a value for something it does not possess.
2. What a Tuple Actually Has
It is worth seeing how short the list is. A tuple has exactly two methods of its own — count and index — and Chapter 6.6 covers both. That is the entire interface beyond reading.
Compare that with the list methods from Chapter 5.3 and 5.5: append, insert, remove, pop, clear, sort, reverse, extend. Every one of those changes the collection. A tuple has none of them, not because they were withheld, but because there is nothing for them to do.
3. What You Do Instead: Build a New One
The tuple you have cannot change. A different tuple can be built from it, and that is the normal way to work:
point = (10, 20)
p2 = point + (30,)
print(p2)
print(point)(10, 20, 30)
(10, 20)point + (30,) built a third tuple; the original is untouched. Note (30,) with its comma — Chapter 6.2's rule, and point + (30) would be the TypeError that chapter showed.
Slicing gives you the same thing from the other direction:
full = (10, 20, 30)
without_last = full[:-1]
print(without_last, full)(10, 20) (10, 20, 30)Nothing was removed. A new tuple was built from the elements the slice selected, which is what Chapter 6.3 said slicing always does.
And if you do not want the old one, point the name at the new one:
p3 = (10, 20)
p3 = p3 + (30,)
print(p3)(10, 20, 30)4. Mutation Versus Rebinding, Again
Chapter 5.3 insisted these are different operations, and for tuples the distinction becomes very clean, because only one of them is even possible.
p3 = (10, 20)
p3 = (99,)
print(p3)(99,)That worked — and nothing was mutated. p3 = (99,) rebinds the name to a different tuple. The original (10, 20) was not altered; it is simply no longer what p3 refers to.
So when somebody says "you can't change a tuple", what they mean is that no operation alters the tuple value. Assigning to the name was never prevented and never will be — names are not tuples, and Chapter 2.3 established that separation long before any of this.
5. The Part the Short Rule Gets Wrong
Here is where "a tuple cannot be changed" stops being sufficient.
A tuple can hold any values, including a list. And a list, as Module 5 spent nine chapters establishing, can be changed:
t = ("config", [1, 2])
print(t)
t[1].append(3)
print(t)('config', [1, 2])
('config', [1, 2, 3])The tuple printed differently the second time. Read that carefully, because the obvious conclusion is the wrong one.
The tuple did not change. Position 0 still refers to the string "config"; position 1 still refers to the same list it always referred to. Nothing about the tuple was altered — t[1] is the identical list object it was before.
What changed is that list. t[1].append(3) reached through the tuple, got the list, and called a list method on it. Chapter 5.3 says append changes the list in place, and it did.
The tuple is still completely rigid about its own job:
t = ("config", [1, 2])
t[1] = [9, 9]TypeError: 'tuple' object does not support item assignmentYou cannot make position 1 refer to a different list. You can only change the list it already refers to.
The same thing seen from outside makes it unmistakable:
inner = [1, 2]
holder = ("config", inner)
print(holder)
inner.append(99)
print(holder)
print(len(holder))('config', [1, 2])
('config', [1, 2, 99])
2holder was never touched after it was built. The list was changed through a completely different name — and because holder refers to that same list, the change is visible through it. That is Chapter 5.8's aliasing, with a tuple as one of the two names.
Note also that the tuple's length never changed — it is 2 before and after. That is the invariant a tuple really gives you: the number of positions and what each one refers to.
6. Why This Decides What Can Be a Key
Chapter 6.1 showed that a tuple can be a dictionary key and a list cannot. Section 5 is what makes that rule precise, and the precise version is more useful than the loose one.
A tuple of immutable values works:
lookup = {("smoke", 1234): "PASS", ("reset", 77): "FAIL"}
print(lookup[("smoke", 1234)])PASSA tuple containing a list does not:
bad = ("config", [1, 2])
d = {bad: "x"}TypeError: unhashable type: 'list'That is the same error a bare list gave in Chapter 6.1 — and the offending type named in the message is list, not tuple. Python looked inside.
The reason follows directly from Section 5. A key has to stay the same so that what you filed can be found again. A tuple of strings and numbers satisfies that, because nothing in it can change. A tuple holding a list does not, because the list can change underneath it — and then the key would no longer match what it matched when you stored it.
7. So What Does Immutability Actually Buy?
It is reasonable to ask what you get for giving up append. Three things, and none of them is "speed".
It cannot be changed behind your back. Chapter 5.8 showed that passing a list around means every holder can change it. Hand someone a tuple of immutable values and the record you gave them is the record they have, permanently. That is a guarantee you can reason with.
It can go where mutable values cannot — as a dictionary key, and as a member of a set in Module 7. Section 6 is that.
It says something to the reader. A tuple in the code announces this grouping is fixed; these positions mean something. A list announces the opposite. Chapter 6.7 turns that into a design decision.
8. Predict, Run, Observe
Predict every line before reading on. One of these changes something and it is not the one most people pick.
a = (1, 2)
b = a + (3,)
print(a)
print(b)
c = ("x", [1])
c[1].append(2)
print(c)
d = (1, 2)
d = (9,)
print(d)The verified output:
(1, 2)
(1, 2, 3)
('x', [1, 2])
(9,)a is unchanged — + built a new tuple, Section 3.
c prints differently, and the tuple still did not change: the list at position 1 did, Section 5. If you predicted an error here, you applied the short rule; if you predicted that c had been modified, re-read what position 1 refers to before and after.
d is (9,) because the name was rebound, Section 4. Nothing was mutated anywhere on that line.
9. Exercises
Exercise 1 — name the error. Predict which error each raises and why, then run them:
(a) p[0] = 1 (b) p.append(1) (c) del p[0] (d) p.sort()
Say in one sentence what distinguishes the TypeError cases from the AttributeError ones.
Exercise 2 — build instead. Starting from point = (10, 20), write expressions producing: a tuple with 30 on the end, a tuple with the first element removed, and a tuple holding only the last element. Confirm point is unchanged after each.
Exercise 3 — predict the nuance. Before running, predict both prints and explain the second in three sentences:
rec = ("smoke", ["PASS"])
rec[1].append("FAIL")
print(rec)
print(len(rec))Exercise 4 — which one changed. For the code in Exercise 3, say precisely what the tuple's position 1 referred to before and after, and whether that changed.
Exercise 5 — the key rule. Predict which of these can be a dictionary key, then verify each and explain any failure:
(a) ("smoke", 1) (b) ("smoke", [1]) (c) ("smoke", ("a", 1)) (d) ["smoke", 1]
Exercise 6 — mutation or rebinding. For t = (1, 2), say which each is and what t holds afterwards:
(a) t = t + (3,) (b) t = (3,) (c) t[0] = 3 (d) t = t[:1]
Exercise 7 — argue the design. A colleague stores a signal record as ("clk", [0, 1]) and says the tuple makes it safe from modification. Write two sentences explaining what is and is not protected, and suggest a change if they want the whole record fixed.
Exercise 8 — what it buys. List the three things immutability genuinely gives you, and name one thing it does not, in one sentence each.
10. Summary
- A tuple cannot be modified. No element replacement, no deletion, no adding, no reordering.
p[0] = xanddel p[0]areTypeError— the operation does not exist for tuples..append(),.remove(),.sort()areAttributeError— the method does not exist at all.- A tuple has exactly two methods,
countandindex. Everything else in the list toolkit changes a collection, and a tuple has no such thing. - To "change" a tuple, build a new one with
+or a slice, and rebind the name if you want. Exactly as Chapter 4.11 taught for strings. - Rebinding a name is always allowed and mutates nothing.
- A tuple fixes which values its positions refer to; it does not freeze those values.
("x", [1])can have its list changed through the tuple or through any other name for that list. - The tuple's length never changes, which is the invariant it really guarantees.
- A value can be a key only if it cannot change, all the way down.
("smoke", 1234)works;("config", [1, 2])raisesTypeError: unhashable type: 'list'. - Immutability buys you three things: a record that cannot be altered behind your back, access to places mutable values are not allowed, and a clear statement of intent to the reader.
- It does not buy meaningful speed, and it does not protect a mutable value you put inside it.
You can read a tuple and you know what it refuses. The next chapter takes one apart into separate names in a single line.
Where this fits
Part of the Python curriculum.
