Which Collections Can Change
Concepts covered: Tuples
Of the 4 collections, 3 can be edited after they exist: you can add items to them or take items out, and the collection keeps its identity through every change. The tuple is the exception. Once built, it holds exactly the items it was built with, for as long as it exists, and every attempt to alter it raises an error.
Editing a list, a dict and a set in place
Editing in place means the operation changes the existing object rather than building a new one. scores.append(90) returns nothing useful and leaves scores longer by 1 item. The same holds for metrics["errors"] = 5 on a dict and seen.add(42) on a set. In the animation below, one dict gains a key, has a value replaced and then loses a key, and the name metrics points at that same dict the whole time.
metrics = {"requests": 1000, "errors": 5}
print("Initial:", metrics)
# Add new key
metrics["latency_ms"] = 45.2
print("After adding:", metrics)
# Update existing key
metrics["requests"] = 1050
print("After update:", metrics)
# Remove a key
del metrics["errors"]
print("After delete:", metrics)Initial: {'requests': 1000, 'errors': 5}
After adding: {'requests': 1000, 'errors': 5, 'latency_ms': 45.2}
After update: {'requests': 1050, 'errors': 5, 'latency_ms': 45.2}
After delete: {'requests': 1050, 'latency_ms': 45.2}
Because the object itself changes, every name bound to it sees the edit. A function that receives a list as a parameter receives the caller's list, so an append inside the function shows up outside it without any return.
def record(batch, item):
batch.append(item)
pending = ["a1", "a2"]
record(pending, "a3")
print(pending)
print(len(pending))Output:
['a1', 'a2', 'a3'] 3
pending has 3 items after the call even though record returned nothing. The function edited the one list that both names refer to.
A tuple cannot be edited
A tuple has neither append nor remove, and it does not allow assignment into a slot. Calling a method it lacks raises an AttributeError, because the tuple has no attribute by that name. Assigning into a slot raises a TypeError, because the tuple refuses item assignment. What you can do is point the name at a different tuple, which leaves the old tuple exactly as it was and is a separate action from editing it.
release = (2024, 11, 3)
print(release[0])
release = (2025, 1, 15)
print(release)Output:
2024 (2025, 1, 15)
Line 3 replaced the whole tuple, which is allowed because assignment binds a name and never touches the object. The table shows what happens when a program instead tries to change the tuple it already has: each line stops the program, and the error type tells you which kind of edit was refused.
| Attempt on release | What Python reports |
|---|---|
| `release.append(2026)` | `AttributeError: 'tuple' object has no attribute 'append'` |
| `release.remove(2025)` | `AttributeError: 'tuple' object has no attribute 'remove'` |
| `release[0] = 2026` | `TypeError: 'tuple' object does not support item assignment` |
Why want something that cannot change
A fixed collection protects data that is supposed to be a fact: a release date, a pair of coordinates, the column names every row has to match. No loop or function anywhere in the program can shorten it by accident. There is a second payoff you met in the sets lesson: only values that cannot change can go into a set or be a dict key, so a tuple works as a key where a list is rejected.
locations = {}
locations[(40.7, -74.0)] = "New York"
print(locations)
print(locations[(40.7, -74.0)])Output:
{(40.7, -74.0): 'New York'}
New YorkPython accepted the tuple as a key, and reading it back with the same tuple returned the stored value. If the same coordinates are written as the list locations[[40.7, -74.0]] = "New York", the program stops with TypeError: unhashable type: 'list'. Unhashable is the name Python gives to values that can change and therefore cannot be filed by their contents.
| Collection | Add | Remove | Replace one item |
|---|---|---|---|
| list | `append` | `remove`, `pop` | assign to a slot |
| dict | assign to a new key | `del`, `pop` | assign to the key |
| set | `add` | `remove`, `discard` | `remove` then `add` |
| tuple | none | none | none, build a new tuple |
Practice
The first exercise is about what a function can do to a list it was handed. The second hands you a tuple that someone treated like a list.
What does this program print?
def finish(tasks):
tasks.pop()
return len(tasks)
todo = ["load", "clean", "report"]
count = finish(todo)
print(count, todo)- 3 ['load', 'clean', 'report']
- 2 ['load', 'clean', 'report']
- 2 ['load', 'clean']
- 2 ['clean', 'report']
An AttributeError raised on a method call usually traces back to the line that built the collection. Parentheses there make a tuple, and square brackets around the same items make a list in the same order, so changing the brackets on that one line is enough for every later append to succeed. Keep the parentheses only when the items are meant to stay fixed.
- Built with parentheses
- ("extract", "clean") is a tuple
- Keeps its items in order
- append raises AttributeError
- Accepted as a dict key
- Built with square brackets
- ["extract", "clean"] is a list
- Keeps its items in order
- append adds to the end
- Refused as a dict key
About this lesson
This section is part of the Data Structures: Beginner lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.