Inventory Lists, a Tuple, and a Set That Will Not Index
An inventory script starts with shelf_a = ["bolt", "nut"], then shelf_b = shelf_a and shelf_c = shelf_a[:]. A helper appends "washer" to shelf_b and removes "bolt" from shelf_c. Later, a report loops over shelf_a and prints three items, which surprises the author, who believed only shelf_b had been changed.
The same script stores a location as point = (3, 5) and later executes point[0] = 4. It also builds parts = {"bolt", "nut", "bolt"} and then runs print(parts[0]). Finally it reads a count with counts = {"bolt": 4} and counts["nut"], which crashes the program.
The author wants each failure explained in terms of mutability, aliasing, ordering, and key lookup before any code is changed, and wants to know which of the three collections is actually the right tool for a fixed coordinate, an unordered unique-parts tally, and a lookup table.
1. Predict the final contents of `shelf_a`, `shelf_b`, and `shelf_c`, and explain why the report over `shelf_a` printed three items.
Read the worked answer
shelf_a and shelf_b end as ['bolt', 'nut', 'washer']; shelf_c ends as ['nut', 'washer'].
shelf_b = shelf_a does not copy anything. It binds a second name to the same list object, so shelf_b is shelf_a is True and the append is visible through both names. This is aliasing, and it is why the report saw three items.
shelf_c = shelf_a[:] creates a new list containing the same element references. shelf_c is shelf_a is False, so remove("bolt") only affects shelf_c. The report over shelf_a was never touched by that removal.
To copy when the elements are themselves mutable, a slice or list(shelf_a) is only a shallow copy: inner lists would still be shared. For plain strings here it is sufficient. A useful diagnostic habit is to print id() or use is when two names seem to disagree about their contents.
2. Name the exception each of `point[0] = 4`, `parts[0]`, and `counts["nut"]` raises, and state the value of `len(parts)`.
Read the worked answer
point[0] = 4 → TypeError. A tuple is immutable, so item assignment is not supported: 'tuple' object does not support item assignment. To change a coordinate, build a new tuple, e.g. point = (4, point[1]). A tuple is the right choice for a fixed coordinate precisely because it cannot be changed by accident.
parts[0] → TypeError. A set is unordered and has no positions, so it is not subscriptable: 'set' object is not subscriptable. Note this is not KeyError. len(parts) is 2, because the duplicate "bolt" collapses on construction. If positions are needed, convert deliberately with sorted(parts) or list(parts) and accept that the order is not meaningful.
counts["nut"] → KeyError. Square-bracket lookup demands the key exist. This is the correct behaviour when a missing key signals corrupt data, and the wrong behaviour when a missing part simply means "zero on hand".
3. Rewrite the count lookup so a missing key is tolerated, and explain the trade-offs between the options.
Read the worked answer
Three safe patterns:
n = counts.get("nut", 0) # 0, no exception
n = counts["nut"] if "nut" in counts else 0
counts.setdefault("nut", 0) # also inserts the key
get(key, default) is the idiomatic read: it returns the default and does not modify the dictionary. Membership testing with in is clearer when the two branches differ substantially. setdefault is the only one that writes back, which is convenient for accumulation (counts.setdefault(k, 0); counts[k] += 1) but surprising if you only wanted to read.
Watch the bare counts.get("nut"): it returns None rather than raising, so a later + 1 produces a confusing TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'. Always pass an explicit default when the value will be used arithmetically, and choose a default whose type matches the stored values.