Mutability

Variables are Pointers

In Python, variables don’t work like buckets that contain objects. Instead, variables are names, like aliases, that refer to objects. Multiple variables can refer to the same object in Python.

You can think of variables in Python as kind of like pointers in C++.

Assigning a list to a new variable does not make a copy, it just makes a new variable that refers to the same object.

>>> nums1 = [1, 2, 3]
>>> nums2 = nums1
>>> nums2[2] = 0
>>> nums2
[1, 2, 0]
>>> nums1
[1, 2, 0]

Objects are the star of the show in Python. Lists, dictionaries, tuples, and other data structures also don’t really “contain” objects, they refer to objects.

Data Structures Contain Pointers

Let’s make a list of three zeroes:

>>> row = [0, 0, 0]

What will this matrix variable be after this?

>>> matrix = [row, row, row]

A list of three lists, with each inner list containing three zeros.

>>> matrix
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]

Which item does this change in this list-of-lists?

>>> matrix[1][1] = 1

The middle item in the middle list?

>>> matrix
[[0, 1, 0], [0, 1, 0], [0, 1, 0]]

It actually changes more than that!

The problem is that when we put row in our list three times, we didn’t copy that list three times, we just referenced the same list three times.

>>> matrix[0] is matrix[1]
True

Lists, dictionaries, tuples, and other data structures also don’t really “contain” objects, they contain pointers to objects.

You can prove this another way, by making a list and trying to append it to itself:

>>> x = []
>>> x.append(x)
>>> x
[[...]]

Python lets us do this, it just puts a reference to the list inside itself.

The Two Types of “Change”

The word “change” is ambiguous in Python. You can change a variable by assigning it to a new value or you can change an object by mutating that object. Usually only one of those changes occurs at a time: we’re either pointing a variable to a value or we’re mutating an object (which one or more variables might point to).

Let’s make a list of numbers, a:

>>> a = [1, 2, 3]

Then let’s say b equals a:

>>> b = a

What is b? And what is a?

>>> b
[1, 2, 3]
>>> a
[1, 2, 3]

They’re both the list [1, 2, 3].

Let’s append 4 to a:

>>> a.append(4)

What is a now?

>>> a
[1, 2, 3, 4]

It’s now the list [1, 2, 3, 4].

And what is b?

>>> b
[1, 2, 3, 4]

It’s also [1, 2, 3, 4]!

Assignment does not copy objects. Assignments point a variable name (new or old) to an object.

What if we take a and b, which both point to the same object:

>>> b
[1, 2, 3, 4]
>>> a
[1, 2, 3, 4]
>>> id(a)  
4564983536
>>> id(b)  
4564983536

And we set a equal to [1, 2]?

>>> a = [1, 2]

What is a now?

>>> a
[1, 2]

What is b now?

>>> b
[1, 2, 3, 4]

We changed the variable a by pointing it to a new object. But that didn’t change b, which still points to the same old object. The id of a has changed because that’s what an assignment does, it points a variable to an object:

>>> id(a)  
4564983285
>>> id(b)  
4564983536

Assignments always point a variable name to an object, there’s never copying or mutating of objects.

Variables and objects are different things in Python: objects are the star of the show and variables just point to objects. Variables are way to give a name to an object so we can reference it later. Multiple variables can point to the same object and objects can even exist without any variables pointing to them at all.

Identity vs Equality

The is operator, which checks identity, will tell us that these variables point to the exact same object in memory:

>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a == b
True
>>> a is b
False

If we check their id, which corresponds to their memory location, we’ll see that it’s the same too:

>>> id(a)  
140218677780480
>>> id(b)  
140218677780424

Copying Lists

Assigning a list to a new variable does not actually make a copy, it just copies the reference:

>>> fruits = ['apples', 'oranges', 'bananas', 'kiwi']
>>> my_fruits = fruits
>>> my_fruits[3] = "kiwi"
>>> my_fruits
['apples', 'oranges', 'bananas', 'kiwi']
>>> fruits
['apples', 'oranges', 'bananas', 'kiwi']

But using the slice notation returns a new list containing just the elements specified. So by extension, one way to completely copy a list would be to use the slice notation and leave off both sides of the slice:

>>> favorite_fruits = fruits[:]
>>> favorite_fruits.pop()
'kiwi'
>>> favorite_fruits
['apples', 'oranges', 'bananas']
>>> fruits
['apples', 'oranges', 'bananas', 'kiwi']

You shouldn’t use a slice to copy a list though, because it is implicit and not immediately obvious. We prefer to be more explicit for this purpose.

You could also copy a list by passing it to the list constructor, which will loop over it and make a new list containing the same items:

>>> favorite_fruits = list(fruits)
>>> favorite_fruits.pop()
'kiwi'
>>> favorite_fruits
['apples', 'oranges', 'bananas']
>>> fruits
['apples', 'oranges', 'bananas', 'kiwi']

But the most preferable way to copy a list in most is to use the list copy method:

>>> favorite_fruits = fruits.copy()
>>> favorite_fruits.pop()
'kiwi'
>>> favorite_fruits
['apples', 'oranges', 'bananas']
>>> fruits
['apples', 'oranges', 'bananas', 'kiwi']

However, if we are copying an iterable that might not be a list, then using the list constructor would be preferable.

Passing Arguments

Say we’re calling a function with a variable as an argument. How does passing an argument to a function work? What does Python do with that variable?

Some languages use pass by value, which will make a copy of the variable, and use that new value to evaluate the function. This takes a bit more time and memory, but ensures that the original variable won’t be changed accidentally. The “copied” value is limited to the scope of the function. This is the default method in C++.

Another way is pass by reference where the function gets an implied reference to the argument variable. This is speedier, and lighter on space, but carries the risk that the variable will be changed outside the scope of the function. C++ supports this behavior, but it’s not the default.

Python isn’t pass-by-value or pass-by-reference. Sometimes people describe Python as “pass-by-assignment”, which is really just a way of saying “calling a function with arguments does the same thing that an assignment does”.

If our input variable is a mutable object, like a list, then it can be modified by the function:

def smallest_n(items, n):
    items.sort()
    return items[:n]

Let’s try it out and see what happens to our input list:

>>> numbers = [3, 1, 8, 9, 2]
>>> smallest_n(numbers, 2)
[1, 2]
>>> numbers
[1, 2, 3, 8, 9]

Our original list has been modified, because the variable inside the function received the calling program’s object reference. We did a mutating method on the input variable, which is also reflected in the calling program’s variable.

Let’s change our function to reassign our input variable to a new object:

def smallest_n(items, n):
    items = sorted(items)
    return items[:n]

Our function doesn’t modify the incoming numbers list:

>>> numbers = [3, 1, 8, 9, 2]
>>> smallest_n(numbers, 3)
[1, 2, 3]
>>> numbers
[3, 1, 8, 9, 2]

It doesn’t matter that we’ve reused the same variable because variables just point to values and we’ve pointed our local items variable to a new value (which sorted returned to us).