Mutable and immutable types¶
Imports¶
import numpy as np
The id()
function¶
A Python variable is a symbolic name that is a reference or pointer to an object. To analyze whether two variables refer to the same object, we can use the id()
function, which returns a unique identification number of the object stored in memory. This will be helpful to see which objects can be changed in-place —i.e. mutable objects— and which cannot.
var = 'Howdy!'
print(f"The id of var is {id(var)}")
print(f"The id of var in hexadecimal format is {hex(id(var))}")
The id of var is 140456188344176 The id of var in hexadecimal format is 0x7fbe81367770
Immutable objects¶
An immutable object is one that cannot be changed after it is created; even when you think you are changing the object, you are really making new objects from old ones. Immutable objects include numbers, strings, and tuples.
For example, if we define an integer and make an in-place sum, the object changes and so does the id
a: int = 1
print(f"The id of a is {hex(id(a))}")
a += 1
print(f"The id of a is {hex(id(a))}")
The id of a is 0x7fbe7e8380f0 The id of a is 0x7fbe7e838110
We can also see what happens if we assign the same object to two different variables and change one of them. At the beginning both variables point to the same address
a: int = 1
b = a
print(f"The id of a is {hex(id(a))}")
print(f"The id of b is {hex(id(b))}")
The id of a is 0x7fbe7e8380f0 The id of b is 0x7fbe7e8380f0
but, as soon as we change b
, a new object is created and now b
points to a different object
b += 1
print(f"{a = }")
print(f"{b = }")
print(f"\nThe id of a is {hex(id(a))}")
print(f"The id of b is {hex(id(b))}")
a = 1 b = 2 The id of a is 0x7fbe7e8380f0 The id of b is 0x7fbe7e838110
Mutable objects¶
Almost everything else is mutable, including lists, dictionaries and user-defined objects. Mutable means that the value has methods that can change the value in-place.
Lists are the paradigmatic example of mutable objects. We can repeat the steps we did in the integer example to see the differences. We begin with an in-inplace sum and, this time, the object's id doesn't change
list_a: list[int] = [1]
print(f"The id of list_a is {hex(id(list_a))}")
list_a += [2] # equivalent to list_a.append(2)
print(f"The id of list_a is {hex(id(list_a))}")
The id of list_a is 0x7fbe813cf740 The id of list_a is 0x7fbe813cf740
Note: beware that if we didn't do an in-place sum but an ordinary sum, a new object would be created.
list_a = [1]
print(f"The id of list_a is {hex(id(list_a))}")
list_a = list_a + [2]
print(f"The id of list_a is {hex(id(list_a))}")
The id of list_a is 0x7fbe813b2a00 The id of list_a is 0x7fbe813cf740
Finally, we show that if two variables point to the same mutable object, any change in the object affects both variables (compare this with the int
example)
list_a = [1]
list_b = list_a
list_b.append(2)
print(f"{list_a = }")
print(f"{list_b = }")
list_a = [1, 2] list_b = [1, 2]
Functions and references¶
As a final example (and warning), an essential feature in Python is that values are passed to functions by assignment. As a consequence, functions can modify global mutable objects even when, apparently, they are just local variables inside the function (local variables are destroyed when the function ends).
def list_append(value, local_list: list) -> None:
local_list.append(value) # list modified
global_list = [1]
list_append(2, global_list)
global_list
[1, 2]
This happens because the (local) variable local_list
gets assigned the same object as the (global) variable global_list
. Since both point to the same mutable object, changes in local_list
will change the object itself, thus changing the object that global_list
is referencing.
But, what happens if local_list
creates a new object? Then local_list
is going to point to a new (local) object and the changes will not affect global_list
.
def list_append(value, local_list: list) -> None:
local_list = local_list + [value] # new list created
global_list = [1]
list_append(2, global_list)
global_list
[1]
And what if local_list
has a default value? Since the line
def list_append(value, local_list = [1]):
is executed only once, local_list
is assigned the default value [1]
when we define the function, not everytime we execute it. So we must be careful: if the object is mutable and we modify it, it will change from one execution to another!
def list_append(value, local_list: list = [1]) -> None:
local_list.append(value)
print(f"local_list = {local_list}")
list_append(2)
list_append(3)
local_list = [1, 2] local_list = [1, 2, 3]
We can fix this pathological behaviour just by creating a new local list instead of modifying it:
def list_append(value, local_list: list = [1]) -> None:
local_list = local_list + [value]
print(f"local_list = {local_list}")
list_append(2)
list_append(3)
local_list = [1, 2] local_list = [1, 3]
If we wanted to initialize a function (or a class) with a default mutable argument, we should set the default value to None
def __init__(list_: list | None = None):
if list_ is None:
list_ = []
...