Mutability and Immutability in Python — Let’s Break It Down

Mutability or immutability

Have you ever modified a variable without knowing it or wanting to?

In this article, we’ll address that by covering id and type, mutable objects, immutable objects, why it matters, how differently does Python treat mutable and immutable objects, how are arguments passed to functions, and what does that imply for mutable and immutable objects. I include examples in Python3.

Who are you, who am I? Photo Attribution: https://www.flickr.com/photos/saulalbert/37545736336

What is id?

id()

Id is a built-in function in Python. It gives us the ability to check the unique identifier of an object. Let’s take a look at how this works.

>>> a = 1
>>> a
1

We see that a is an object that contains the value 1. Now let’s check the id, the unique identifier, of that object.

>>> id(a)
10105088

The unique identifier is pointing to a location in memory, which is an object. Let’s try another one.

>>> b = 2
>>> b
2
>>> id(b)
10105120

You can see that id(a) is different from id(b) but we can also run a test.

>>> a = 1
>>> b = 2
>>> id(a) == id(b)
False

Let’s assign another variable to 2 and check the id.

>>> a = 1
>>> b = 2
>>> c = 2
>>> c
2
>>> id(c)
10105120

Hmm, id(c) looks the same as id(b). Why is that? Let’s run a test.

>>> id(c) == id(b)
True

They have the same unique identifier because c is referencing an object that contains the value 2, and b is also referencing the same object that contains the value 2. Okay, let that sink in for a moment. Wow, right?

They’re both pointing to the same object that contains a value 2.

>>> id(c)
10105120
>>> id(b)
10105120

The object, 10105120, is the unique identifier. It’s the location in memory. Objects can have multiple variables. If you’re debugging a program, this might be a useful function. Let’s now try this with strings.

>>> a = "strawberry"
>>> b = "strawberry"
>>> id(a) == id(b)
True
>>> a = "strawberry"
>>> b = "Strawberry"
>>> id(a) == id(b)
False

What happened here? In the second example above, we used a capital S in variable b’s object. That’s why it was False.

Let’s look at more examples.

>>> a = 256
>>> b = 256
>>> id(a) == id(b)
True
>>> a = 260
>>> b = 260
>>> id(a) == id(b)
False

Why is it True for 256 but False for 260?

The reason is that Python keeps an array of integer objects for all integers between -5 and 256. When you create an integer in that range, you get back a reference to the already existing object.

It does this with two macros, NSMALLNEGINTS and NSMALLPOSINTS. If the value ival satisfies the condition of being between -5 and 256, the function get_small_int is called.

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS 257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS 5
#endif
#define CHECK_SMALL_INT(ival)
do if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {
return get_small_int((sdigit)ival);
} while(0)

Note: You can also use “is” to check if two variables have the same object id.

>>> list1 = [1, 2, 3]
>>> list2 = list1
>>> list1 is list2
True

Let’s introduce some more terminology.

The same list above has two different names, list1 and list, and we can say that it is aliased. Variables refer to objects and if we assign one variable to another, both variables refer to the same object. That is what aliasing means.

Let’s also talk about cloning. If we want to modify a list and also keep a copy of the original, we need to make a copy of the list. This process is called cloning. Taking any slice of a list creates a new list.

>>> list1 = [1, 2, 3]
>>> list2 = list1[:]
>>> list1
[1, 2, 3]
>>> list2
[1, 2, 3]
>>> list1 is list2
False
>>> id(list1) == id(list2)
False
Types

What is type?

type()

In Python, all data is stored in the form of an object. An object has three things: id, type, and value.

The type function will provide the type of the object that’s provided as its argument.

>>> type(2)
<class 'int'>
>>> type(-6.25)
<class 'float'>
>>> type(2,)
<class 'tuple'>
>>> type("hello")
<class 'str'>
>>> type('A')
<class 'str'>
>>> type('346.789')
<class 'str'>
>>> type([2, 3, 4])
<class 'list'>
>>> type({'category': 'produce', 'count': 200})
<class 'dict'>
>>> type(print)
<class 'builtin_function_or_method'>
>>> type(type)
<class 'type'>

Like id(), type() is also useful for debugging.


What are mutable objects?

Python
Mutable objects:
list, dict, set

A program stores data in variables that represent the storage locations in the computer’s memory. The contents of the memory locations, at any given point in the program’s execution, is called the program’s state.

Some objects in Python are mutable, and some are immutable. First, we’ll discuss mutable objects. A mutable object is a changeable object and its state can be modified after it is created.

>>> my_list = ['cat', 'dog', 'bunny']
>>> my_list
['cat', 'dog', 'bunny']
>>> print('Address of my_list is: {}'.format(id(my_list)))
Address of my_list is: 139929780579208

If we want to change the first value in our list and print it out, we can see that the list changed, but the memory address of the list is the same. It changed the value in place. That’s what mutable means.

>>> my_list[0] = 'sugar glider'
>>> my_list
['sugar glider', 'dog', 'bunny']
>>> print('Address of my_list is: {}'.format(id(my_list)))
Address of my_list is: 139929780579208

Let’s look now at the memory addresses of values of the list and see what happens before and after we change the value of the first element of the list.

>>> my_list
['sugar glider', 'dog, 'bunny']
>>> id(my_list)
139929780579208
>>> my_list[0]
'sugar glider'
>>> id(my_list[0])
139905997708792
>>> my_list[0] = 'rabbit'
>>> id(my_list[0])
139905997708400
>>> id(my_list)
139929780579208

The id of my_list[0] is 139905997708792 when the value of the first element is ‘sugar glider’. The id of my_list[0] is 139905997708400 after we change the value to ‘rabbit.’ Notice they are two different ids.

When we modify a list and change its values in place, the list keeps the same address. However, the address of the value that you changed will have a different address.

The id of my_list still remained the same at 139929780579208.

What are immutable objects?

Python
Immutable objects:
integer, float, string, tuple, bool, frozenset

An immutable object is an object that is not changeable and its state cannot be modified after it is created.

In Python, a string is immutable. You cannot overwrite the values of immutable objects.

However, you can assign the variable again.

>>> phrase = 'how you like me now'
>>> phrase
how you like me now
>>> phrase = 'do you feel lucky'
>>> phrase
do you feel lucky

It’s not modifying the string object; it’s creating a new string object.

To see this in more detail, we can utilize the id function that we learned earlier. Recall that id() function prints out the memory address.

>>> phrase = 'how you like me now'
>>> print('Address of phrase is: {}'.format(id(phrase)))
Address of phrase is: 139929793080104
>>> phrase = 'do you feel lucky'
>>> print('Address of phrase is: {}'.format(id(phrase)))
Address of phrase is: 139929792606832

Since a string is immutable, it created a new string object. The memory addresses do not match.

Let’s try to change a single character of the phrase.

>>> phrase[0] = 'D'
...
TypeError: 'str' object does not support item assignment

We get a TypeError because strings are immutable. We can’t change the string object.

Let’s also talk about tuples.

Immutability on tuples is only partly true. The tuple itself cannot be modified, but objects referenced by the tuple can be modified. If the tuple has an immutable field like a string, then the tuple cannot be modified and it is sometimes called “non-transitive immutability.” But a mutable field like a list can be edited, even if it’s embedded in the “immutable” tuple.

Why do mutable and immutable objects matter and how differently does Python treat them?

Numbers, strings, and tuples are immutable. Lists, dictionaries, and sets are mutable, as are most new objects you’ll code with classes.

Immutability may be used to ensure that an object remains constant throughout your program. The values of mutable objects can be changed at any time and place, whether you expect it or not.

You can change a single value of a mutable data type and it won’t change its memory address. However, you can’t change a single value of an immutable type. It will throw an error.


How are arguments passed to functions and what does that imply for mutable and immutable objects?

The way that the Python compiler handles function arguments has to do with whether the objects in the function arguments are mutable or not immutable.

If a mutable object is called by reference in a function, the original variable may be changed. If you want to avoid changing the original variable, you need to copy it to another variable.

When immutable objects are called by reference in a function, its value cannot be changed.

Let’s look at this Python script and guess what it will print:

def increment(n):
n += 1

b = 9
increment(b)
print(b)

Think about it and then continue reading for the answer.

The variable b refers to the object with value 9. When we pass b as a function argument to increment(n) function, the local variable n refers to the same object. However, integers are immutable so we need to create a new object with the value 10 and assign it to the variable n. The variable n is pointing to a different object from what b is pointing. Now, n refers to an object with value 10, but b still refers to an object with value 9. When we print(b), we get the answer 9.

The answer: 9


Let’s look at another Python script and guess what it will print:

def increment(n):
n.append(4)

my_list = [1, 2, 3]
increment(my_list)
print(my_list)

Think about it, perhaps draw a visualization, and then continue reading for the answer.

The variable my_list refers to a list object that contains references to three integers. Lists are mutable but integers are immutable. When we pass my_list as a function argument to increment(n) function, the function has the local variable n refer to the same object that my_list refers.

Since lists are mutable, the .append() method is able to modify the list in place. No new object is created and when we print my_list, we get the answer [1, 2, 3, 4].

The answer: [1, 2, 3, 4]


Let’s look at another Python script to understand more about function parameters and why mutability and immutability matter.

def assign_value(n, v):
n = v

list1 = [1, 2, 3]
list2 = [4, 5, 6]
assign_value(list1, list2)
print(list1)

Think about it and then continue reading for the answer.

We pass both lists as function parameters to the assign_value(n, v) function. The function has the local variable n refer to the same object that list1 refers, and the local variable v refers to the same object that list2 refers.

The function body reassigns n to what v is referring. Now n and v are referring to the same object.

The variables n, v, and list2 all point to the list object [4, 5, 6], while list1 still points to the list object [1 2, 3]. This is why when we print list1, we get the answer: [1, 2, 3]

The answer: [1, 2, 3]


How do we write a function that returns a copy of a list? Here’s one way of doing it. Let’s look at copy_list(l) function.

def copy_list(l):
return l[:]

my_list = [1, 2, 3]
new_list = copy_list(my_list)

We pass my_list as a function parameter to the copy_list(l) function. The function has the local variable l refer to the same object that my_list refers. When we use the slice operation [:], it creates a copy of a list and when we return that copy, we are returning the reference to that copy. Now, new_list refers to a different object than what my_list refers.

>>> def copy_list(l):
... return l[:]
...
>>> my_list = [1, 2, 3]
>>> new_list = copy_list(my_list)
>>> my_list
[1, 2, 3]
>>> copy_list
[1, 2, 3]
>>> my_list == copy_list
True
>>> my_list is copy_list
False
>>> id(my_list) == id(copy_list)
False

I know we covered a lot here, so take a deep breath, practice, repeat, and you’ll soon be able to explain these concepts to other people. Thanks for reading!