Exercises — Tuples — immutability, use cases
Everything here builds on the parent note Mutable vs Immutable objects and connects to Lists — mutability, methods, Dictionaries — keys and values, Sets — hashing & uniqueness, Functions — return values, and Hashing in Python.

Read the figure: on the left, the two arrows out of the tuple are painted with a lock — they can never be re-pointed — yet the list they reach into is open, so t[1].append(4) still works. On the right, each item passes its own hash up the chain; the int and string succeed (green), but the list has no stable hash (red), so the tuple's hash computation fails and it cannot be a dict key or set member.
Level 1 — Recognition
L1.1 — Which of these is a tuple?
Recall Solution
The comma builds a tuple, not the parentheses. Walk each line:
a = (3)→ the parentheses only group; there is no comma → this is the int3. Not a tuple.b = (3,)→ trailing comma → a 1-element tuple(3,). ✅ Tuple.c = 3, 4→ a comma with no parentheses → Python packs it → tuple(3, 4). ✅ Tuple.d = ()→ the one exception: empty parentheses are the empty tuple(). ✅ Tuple.e = [3, 4]→ square brackets → a list. Not a tuple.f = "3, 4"→ quotes → a string (the comma is just a character inside text). Not a tuple.
Answers: tuple = b, c, d. Not tuple = a (int), e (list), f (str).
L1.2 — What prints?
Recall Solution
type(x).__name__ gives the type name as a plain word.
(5)has no comma → it's the int5→ name isint.(5,)has a comma → it's a 1-tuple → name istuple.
Output: int tuple
Level 2 — Application
L2.1 — Read-only operations
Recall Solution
Index positions: t[0]=5, t[1]=3, t[2]=5, t[3]=8, t[4]=5.
len(t)→ 5 (five items).t[1]→ 3 (position 1, counting from 0).t[-1]→ 5 (last item;-1counts from the end).t[1:4]→ items at positions 1, 2, 3 →(3, 5, 8)(a new tuple; the stop index 4 is excluded).5 in t→True(5 appears).t.count(5)→ 3 (5 occurs three times).t.index(8)→ 3 (first — and only — 8 is at position 3).
L2.2 — Concatenate and repeat
Recall Solution
t + (3,)→(1, 2, 3)— a brand-new tuple joining the two.t * 3→(1, 2, 1, 2, 1, 2)— the tuple repeated three times, again brand-new.tafterwards →(1, 2), unchanged.
Key point: + and * never edit a tuple; they build a new one. Since we never rebound t, the original still holds (1, 2).
Level 3 — Analysis
L3.1 — The forbidden write
Recall Solution
All three try to mutate, and tuples forbid mutation:
t[0] = 99→TypeError('tuple' object does not support item assignment). Assigning to an index means "overwrite slot 0" — banned.t.append(4)→AttributeError('tuple' object has no attribute 'append').appenddoesn't exist on tuples; onlycountandindexdo.del t[0]→TypeError('tuple' object doesn't support item deletion). Deleting a slot is a mutation — banned.
Takeaway: the type of error differs (TypeError vs AttributeError), but the reason is identical: each operation would change the tuple.
L3.2 — Shallow immutability
Recall Solution
Immutability is shallow — it fixes which object each slot points to, not the insides of those objects (recall the "fixed arrows" picture at the top).
t[1].append(4)→ succeeds. Slot 1 points at a list; we don't touch the slot, we change the list it points to. Nowt == (1, [2, 3, 4]).t[1] = [9]→TypeError. This tries to make slot 1 point at a different object — that's editing the tuple's slot, which is forbidden.
Result of the successful line: t == (1, [2, 3, 4]).
Level 4 — Synthesis
L4.1 — Hashability decides dict-key legality
Recall Solution
A dict key must be hashable. A tuple is hashable only if every item inside it is hashable — the "inherited hash" chain in the top figure (see Hashing in Python).
(0, 0)→ items are ints (immutable, hashable) → ✅ valid key.(1, "x", 3.5)→ int, str, float all hashable → ✅ valid key.(1, [2, 3])→ contains a list (mutable, unhashable) → the chain breaks → the whole tuple is unhashable → ❌TypeError.[0, 0]→ a list is mutable → unhashable → ❌TypeError.((0, 0), (1, 1))→ a tuple of tuples, all items immutable → ✅ valid key.
Rule: hashability is inherited — a container is hashable exactly when all its contents are.
L4.2 — Multiple returns and swapping
Recall Solution
Step through it (this uses Functions — return values):
stats([4,1,9,2])returns the tuple(1, 9, 16)— Python packs the three values.- Unpacking:
lo=1, hi=9, total=16. lo, hi = hi, lo→ the right side first forms the temporary tuple(9, 1), then unpacks it →lo=9, hi=1. No temp variable needed.totalis untouched →16.
Output: 9 1 16
Level 5 — Mastery
L5.1 — Design a sparse grid
Recall Solution
Design: use a dictionary keyed by the coordinate tuple (row, col). The figure below shows the sparse grid on the left and the resulting dictionary on the right — only filled cells become entries, so empty cells cost nothing (b), and dict lookup by hash is fast (c). This combines Dictionaries — keys and values with tuple immutability.

Read the figure: each shaded cell in the grid is labelled by its (row, col) tuple; on the right, that same tuple is the key (locked, immutable, drawn in the item's colour) and the arrow points to its stored mark. Because the keys can never change identity, the dict can always find them again.
Why a tuple key: a coordinate is a fixed pair of different meanings (row, col) that must never change identity. Because it's immutable it is hashable → a legal key. A list [0, 0] would raise TypeError.
Trace:
board[(0,0)] = "X"→ adds entry.board[(1,2)] = "O"→ adds a second entry.board[(0,0)] = "O"→ same key(0,0)already exists (equal tuples have equal hashes), so it overwrites, not adds.board[(0,0)]→"O";len(board)→2(two distinct keys).
Output: O 2
L5.2 — Tuples in a set (dedup coordinates)
Recall Solution
A set stores only distinct hashable items (see Sets — hashing & uniqueness). Duplicate tuples collapse to one because equal tuples share a hash and compare equal.
- Adding
(0,0),(1,1),(0,0),(2,3),(1,1). - Distinct values:
(0,0),(1,1),(2,3)→ 3 unique points.
Output: 3
Why tuples: set elements must be hashable. Coordinate lists would raise TypeError; tuples of ints are immutable → hashable → legal.
L5.3 — Unpacking must match in length
Recall Solution
Plain unpacking demands the count on the left match the tuple length exactly:
- A:
(1, 2)has two items, two names on the left →a=1, b=2. Succeeds. - B:
(1,)has one item but two names →ValueError: not enough values to unpack (expected 2, got 1). - C:
(1, 2, 3, 4)has four items but three names →ValueError: too many values to unpack (expected 3). - D: the
*rest"star target" soaks up the leftovers →a=1,rest=[2, 3](note:restis a list, not a tuple). Succeeds.
Rule: without a *, len(left) == len(right) is mandatory; a single *name absorbs zero-or-more items so the match always works.
L5.4 — Slicing surprises: negative step & out-of-range
Recall Solution
Slicing never raises for out-of-range bounds — it just clamps to what exists, and always returns a new tuple.
- A:
t[::-1]→ step-1walks backwards →(50, 40, 30, 20, 10)(the reverse). - B:
t[1:100]→ stop100is past the end, so it's clamped to the length →(20, 30, 40, 50). No error. - C:
t[3:1]→ start3is after stop1with the default+1step → nothing to collect →()(empty tuple), not an error. - D:
t[-2:]→ start two from the end, run to the end →(40, 50). - E:
t[::2]→ every second item from the start →(10, 30, 50).
Rule: out-of-range slice indices are safe (clamped/empty); only direct indexing like t[100] raises IndexError.
L5.5 — Full edge-case sweep
Recall Solution
Cover the degenerate inputs explicitly — note A and B are different questions:
- A: the expression
()evaluates to the empty tuple()itself — an object with zero items. (Its length is a separate question, answered in B.) - B:
len(())→0(the empty tuple contains zero items). - C:
(42,)is a 1-tuple;(42)is the int42. A tuple is never equal to an int →False. - D:
("a",) * 0→ repeating zero times gives the empty tuple(). (Zero or negative repeat counts yield an empty tuple, never an error.) - E:
(1, 2) + ()→ concatenating with the empty tuple changes nothing →(1, 2). - F:
t[100]on a 2-item tuple →IndexError— direct indexing past the end does raise (unlike slicing in L5.4).
These are the boundary cases (empty value vs empty length, zero-repeat, tuple-vs-scalar, out-of-range index) you must never be surprised by.
Recall
Recall One-line self-test
Design decision ::: fixed record or dict/set key → tuple; growing homogeneous data → list.
Why can't (1, [2]) be a key ::: it holds a list (mutable) → unhashable → TypeError.
What does a, b = b, a build first ::: the temporary tuple (old_b, old_a), then unpacks it.
Result of ("x",) * 0 ::: the empty tuple ().
Result of a, b = (1,) ::: a ValueError — not enough values to unpack (expected 2, got 1).
Result of t[::-1] for t=(10,20,30) ::: the reversed tuple (30, 20, 10).
Does t[1:100] raise for a 5-item tuple ::: no — out-of-range slice bounds are clamped, giving the tail.
Connections
- Lists — mutability, methods — the mutable sibling you contrast against in every choice.
- Dictionaries — keys and values — tuples as immutable keys (L4.1, L5.1).
- Sets — hashing & uniqueness — hashable membership (L5.2).
- Functions — return values — pack/unpack multiple returns (L4.2).
- Mutable vs Immutable objects — the concept behind shallow immutability (L3.2).
- Hashing in Python — why immutability enables keys and set members.
Is (42,) == (42)?
What is ("a",) * 0?
(); zero repeats give an empty tuple, no error.Which line succeeds: t[1].append(4) or t[1] = [9] for t = (1, [2, 3])?
t[1].append(4) succeeds (mutates the inner list); t[1] = [9] raises TypeError.Why does a, b = b, a swap without a temp?
a and b.What happens with a, b = (1,)?
What does t[::-1] do?
t.Does a slice like t[1:100] raise for out-of-range bounds?
t[100] raises IndexError.