Show how mutating a list affects every name that refers to it, and why a tuple behaves differently.
Mutability is about whether the object itself can be changed after it has been created. Lists, dictionaries, sets and bytearrays are mutable. Tuples, strings, bytes, frozensets and numbers are not. When two names point at the same mutable object, a change made through one name is visible through the other name, because both names refer to one single object.
def add_item(items):
items.append('added')
return items
original = ['a', 'b']
alias = original
original.append('c')
print(original) # ['a', 'b', 'c']
print(alias) # ['a', 'b', 'c']
print(original is alias) # True
# A tuple has no append, so you build a new one instead.
pair = (1, 2)
pair = pair + (3,)
print(pair) # (1, 2, 3)
print(len(pair)) # 3
The alias shows the key point, because there is one list object here and two names for it. The tuple lines show the other point. Because a tuple cannot be changed in place, the only way to extend it is to build a new tuple, and then the name is bound to that new tuple.
This distinction is why the words change and rebind matter so much. Mutating a list changes the object that every other reference sees. Rebinding a name only changes which object that one name points at. A list can also be rebound, and when that happens the other names keep on seeing the old object.
That is exactly the mechanism behind the classic bug where a default argument of None or an empty list collects state between calls, and it is also the reason that copy.deepcopy exists for nested structures.