Return Values and Object Mutations

We are accustomed to the idea that when a function or method creates a new object, we have to assign that new object to a variable if we want to save it. For example, if we run the following code:

[1]:
x = [1, 2, 3]
x = x + [4]
x
[1]:
[1, 2, 3, 4]

We know that the variable x will end up pointing at the new list ([1, 2, 3, 4]). That’s because a function or method that creates a new object “returns” that object.

But when we mutate an object, the newly mutated object won’t be returned to us. Instead, the method will return either nothing (None), or sometimes it will return something entirely different. For example, the command x.pop(0) would actually do two things:

  1. Remove the item at index 0 from the list x (mutating it in place)

  2. Return whatever item has been removed.

So if we run:

[2]:
y = x.pop(0)
y # We get back the list item at index 0 (which was 1)
[2]:
1
[3]:
x # And the list has mutated.
[3]:
[2, 3, 4]

But this can lead to an easy problem: if we mutate our list using function or method that mutates the list in-place, and re-assign the result of that method call to our original variable, we’ll actually lose track of our original list!

[4]:
x = [1, 2, 3]
x = x.pop(0)
x
[4]:
1

Tuples

OK, so if tuples are immutable, how did that tuple seem to mutate?

The answer is that, from Python’s perspective, the tuple itself didn’t mutate. It contained references to three objects: the number 1, the number 2, and a specific list. That list mutated when we ran x.append(-2), but the tuple did not: the tuple never stopped pointing to the same list.

This is the tricky thing about recursive data structures: what they hold is always a reference to an object.