1.2.23 · D4Introduction to Programming (Python)

Exercises — Tuples — immutability, use cases

3,389 words15 min readBack to topic

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.

Figure — Tuples — immutability, use cases

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 int 3. 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 int 5 → name is int.
  • (5,) has a comma → it's a 1-tuple → name is tuple.

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; -1 counts 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 tTrue (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.
  • t afterwards → (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] = 99TypeError ('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'). append doesn't exist on tuples; only count and index do.
  • 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. Now t == (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.
  • total is 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.

Figure — Tuples — immutability, use cases

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: rest is 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 -1 walks backwards → (50, 40, 30, 20, 10) (the reverse).
  • B: t[1:100] → stop 100 is past the end, so it's clamped to the length → (20, 30, 40, 50). No error.
  • C: t[3:1] → start 3 is after stop 1 with the default +1 step → 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 int 42. 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 Pythonwhy immutability enables keys and set members.

Is (42,) == (42)?
False — a 1-tuple is not equal to the int 42; different types.
What is ("a",) * 0?
The empty tuple (); 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?
The right side is packed into a tuple first, then unpacked back into a and b.
What happens with a, b = (1,)?
ValueError — not enough values to unpack (expected 2, got 1).
What does t[::-1] do?
Returns a new tuple that is the reverse of t.
Does a slice like t[1:100] raise for out-of-range bounds?
No — slice bounds are clamped; only direct indexing like t[100] raises IndexError.