A Surprising Feature of Python Lists
What do you expect the output of the following Python code to be?
def mutate_or_not(a_list):
a_list[0] = "I've changed"
my_list = [1, 2, 3]
mutate_or_not(my_list)
print(my_list)
Depending on how well you understand how Python works under the hood, you may be surprised at this result. You might reason that since my_list
is defined in the global scope, it has no business being changed when its value is passed into mutate_or_not()
as a paramter. Let’s explore what is happening here.
You can step through the code with a great online visualization tool here.
Notice how the list is pointed to by both the
my_list
variable in the global frame AND the thea_list
parameter insidemutate_or_not()
.
Compare the situation above to the following:
def mutate_or_not(an_int):
an_int = 7
my_int = 5
mutate_or_not(my_int)
print(my_int)
>>> 5
Look at the image below:
Code tracing available here. Can you see how there is no link between my_int
(in the global frame), and an_int
, the function parameter?
A Practical Example of Python List Mutation
Suppose you wish to rotate the items in a list. After some thinking, you might come up with a solution like this:
def rotate_list(lst, n):
n = n % len(lst)
lst = lst[-n:] + lst[:-n]
s1 = [1, 2, 5, 4]
rotate_list(s1, 1)
print(s1)
You might expect the output to be a successfully rotated list: [4, 1, 2, 5]
. However, it doesn’t work!
But didn’t you just say that lists were mutable?
The issue here is that when we reassign lst
inside the function, the original assignment is lost, and lst
now just references the local parameter lst
with the new values. The original lst
defined outside of the function remains intact.
So what can we do about this? Well one solution is given below, but it is very clumsy and not practical. It does illustrate the issue though so it’s worth looking at:
def rotate_list_2(lst):
lst[0], lst[1], lst[2], lst[3] = lst[3], lst[0], lst[1], lst[2]
s1 = [1, 2, 5, 4]
rotate_list_2(s1)
print(s1)
>>> [4, 1, 2, 5]
The reason this works is that we do not redefine lst
inside the function. Instead we mutate its elements.
However a much better solution is to use Python list slicing, as discussed for example here. In Python, my_list[:]
refers to the whole list. Using this fact, we can rewrite our rotate_list()
function and leverage the immutability of Python lists to achieve the desired result:
def rotate_list(lst, n):
n = n % len(lst)
lst[:] = lst[-n:] + lst[:-n]
s1 = [1, 2, 5, 4]
rotate_list(s1, 1)
print(s1)
Mutable and Immutable Data Types in Python
Mutability in Python doesn’t just apply to lists and integers, but other data types as well. We just focused on lists in this article to keep things programmatic. Below is a table for easy reference.
mutable | immutable | |
---|---|---|
list | ✓ | |
dictionary | ✓ | |
set | ✓ | |
user-defined classes | ✓ | |
int | ✓ | |
float | ✓ | |
decimal | ✓ | |
bool | ✓ | |
string | ✓ | |
tuple | ✓ | |
range | ✓ |
Conclusion
This article has been about the mutability of lists in Python programming. Knowing how this works will help you to avoid some potentially confusing and time-consuming bugs in your code.
Happy coding!