Show that a function can mutate a list passed to it, but cannot rebind the caller's name to something else.
Both halves of the answer are visible in two small functions.
def append_item(items):
items.append('added')
return len(items)
def replace_items(items):
items = ['new']
return items
original = ['a']
print(append_item(original)) # 2
print(original) # ['a', 'added'] - changed in place
original = ['a']
print(replace_items(original)) # ['new']
print(original) # ['a'] - the caller's list is untouchedThe first function changes the list that the caller owns, because the function received a reference to that same list object and then changed it. The second function rebinds its own parameter name to a brand new list, which changes nothing outside the function, so the caller's name still refers to the original list.
The reason is that a parameter is a local name inside the function, exactly like any other local. Assigning to it creates a new binding in the function's own scope. The important distinction is between changing an object, which everybody who holds a reference sees, and rebinding a name, which only affects the scope where the rebinding happens.
That is also why the practical advice is to copy the data before you pass it if the function should not be able to modify it. A shallow copy with list(items) or items.copy() is enough for a flat list. For nested structures you need copy.deepcopy, because a shallow copy still shares the inner objects. Alternatively, pass an immutable value such as a tuple, or make it part of the function's contract that it is allowed to change its argument.