PYTHON / LISTS AND TUPLES
Tuples and immutability
Create tuples correctly, predict which operations raise TypeError, and use tuples as dictionary keys and set members.
What you will learn
- Write a one-element tuple as (7,) and know that the comma, not the parens, builds it
- Predict TypeError on item assignment and AttributeError on append/sort for tuples
- Explain why a tuple of ints is hashable but a tuple holding a list is not
- Recognise that t += (x,) rebinds a new tuple while lst += [x] mutates in place
Understanding Tuples and immutability
A tuple is a fixed-length block of references to objects. Once it is built, the number of slots and which object each slot points at can never change, so there is no tuple equivalent of append, insert, pop, sort, or reverse, and index assignment raises TypeError. That fixed layout is why CPython can allocate a tuple in one piece and why tuples are slightly smaller and faster to build than lists holding the same items.
Immutability applies to the slots, not to the objects in them. If a slot points at a list, you cannot swap that list for a different one, but you can still call append on it, and the tuple's printed value changes as a result. This is often called shallow immutability, and it is exactly why hash(('a', [1])) fails: hashing a tuple hashes each element in turn, and a list has no hash because its value can change.
Hashability is the practical payoff. Because a tuple of immutable items has a value that can never change, its hash can never go stale, so tuples work as dictionary keys and set members where lists do not. That makes tuples the natural way to key a grid by (row, col), a record by (last_name, first_name), or a cache by its argument list. Note that += on a tuple does not extend anything: it builds a brand-new tuple and rebinds the name, so growing a tuple inside a loop copies everything on each pass.
point = (3, 4)
print(point[0], len(point))
try:
point[0] = 99
except TypeError as e:
print("TypeError:", e)
single = (7,)
not_a_tuple = (7)
print(type(single).__name__, type(not_a_tuple).__name__)
labels = {(3, 4): "target", (0, 0): "origin"}
print(labels[point])A tuple permanently fixes which objects occupy its slots, which makes it hashable but does not freeze the objects themselves.
Worked examples
Shallow immutability
Shows that a list inside a tuple can still be modified, and what that costs you.
row = ([1, 2], "fixed")
row[0].append(3)
print(row)
try:
{row: "key"}
except TypeError as e:
print("TypeError:", e)
print({(1, 2, "fixed"): "key"}[(1, 2, "fixed")])Example explained
Line 1row[0].append(3) never touches the tuple's slots, so it is allowed; slot 0 still points at the same list.
Line 2Using row as a dict key fails because hashing the tuple must hash the list inside it, and lists are unhashable.
Line 3Replacing the list with plain ints and a string makes every element hashable, so the tuple works as a key.
+= rebinds a tuple but mutates a list
Compares identity before and after augmented assignment on a tuple versus a list.
t = (1, 2)
before = id(t)
t += (3,)
print(t, id(t) == before)
nums = [1, 2]
before_list = id(nums)
nums += [3]
print(nums, id(nums) == before_list)Example explained
Line 1t += (3,) builds a new 3-slot tuple and rebinds t, so the id differs from before.
Line 2The original (1, 2) is untouched; any other name still bound to it sees two items.
Line 3nums += [3] calls list extend in place, so the id is unchanged and every alias of nums sees the new item.
What methods a tuple actually has
Shows the two tuple methods and how to sort tuple data without mutating it.
grades = (90, 72, 90, 85)
print(grades.count(90), grades.index(85))
print(sorted(grades))
print(tuple(sorted(grades)))
print(hasattr(grades, "sort"), hasattr(grades, "append"))Example explained
Line 1count and index only read the tuple, so they are the only two methods tuples keep from the list API.
Line 2sorted() always returns a new list, even when given a tuple, because it cannot reorder fixed slots.
Line 3Wrap the result in tuple() when you need the sorted data to stay hashable.
Line 4hasattr confirms sort and append do not exist, so calling them raises AttributeError, not TypeError.
Important notes
Tuple equality and hashing compare element values, so (1, 2) == (1.0, 2.0) is True and both index the same dict entry.
Immutability is not a security boundary: a tuple only guarantees its own slots, and it is trivially replaced by rebinding the name that holds it.
Common mistakes
Writing (7) and expecting a tuple: the parens are just grouping, so you get an int, and the next len() or indexing call raises TypeError: object is not subscriptable.
Assuming a tuple freezes its contents, then storing a list inside one and handing it out as "safe" data; callers mutate it and every holder of the tuple sees the change.
Calling grades.sort() or grades.append(x) on a tuple, which raises AttributeError, or using sorted() and assigning the resulting list where a hashable key was required.
Try it yourself
Change, predict, then run
Build a dict that maps (row, col) tuples to characters for a 2x2 grid, print the value at (1, 0), then try to assign into one of the key tuples inside a try/except and print the TypeError message.
Open the Python workspaceCheck your understanding
A function receives config = ("v1", ["a", "b"]) and calls config[1].append("c"), which succeeds. Why?
- The tuple only fixes which objects its slots reference, not the internal state of those objects
- A tuple becomes mutable as soon as one of its elements is mutable
- Indexing a tuple returns a copy, so the append affects that copy rather than the stored list
- Appending is allowed because it does not change the tuple's length
Show answer
Slot 1 keeps pointing at the same list object the whole time; append changes that list, not the tuple, so nothing about the tuple's structure is violated. Option 3 sounds close because the tuple's length really is unchanged, but that is a consequence, not the reason: config[1] = [] fails even though it also leaves the length at 2, since it would replace which object the slot references.