Python | Mutable, Immutable

Before reading the concepts, we must understand that everything, absolutely everything in Python is an object, and when we have it clear that we can understand that objects can be both mutable and immutable.

Lets dive deeper into the details of it… Since everything in Python is an Object, every variable holds an object instance. When an object is initiated, it is assigned a unique object id. Its type is defined at runtime and once set can never change, however its state can be changed if it is mutable. Simple put, a mutable object can be changed after it is created, and an immutable object can’t.

Now comes the question, how do we find out if our variable is a mutable or immutable object. For this we should understand what ‘ID’ and ‘TYPE’ functions are for.

ID and TYPE

The built-in function id() returns the identity of an object as an integer. This integer usually corresponds to the object’s location in memory, although this is specific to the Python implementation and the platform being used. The is operator compares the identity of two objects. For example

SYNOPSIS: id(object)

>>> hi = "Hello"
>>> name = "Gera"
>>> id(hi)
140161527368240
>>> id(name)
140161527369200
>>> print(hi is name)
False
>>> name = "Hello"
>>> print(hi is name)
True
>>> id(name)
140161527368240
>>>

The built-in function type() returns the type of an object. Lets look at a simple example

SYNOPSIS : type(object)

>>> type(1)
<class 'int'>
>>> type("Holberton")
<class 'str'>
>>> a = 15
>>> type(a)
<class 'int'>
>>>

We have now seen how to compare two simple string variables to find out the types and id’s .So using these two functions, we can check to see how different types of objects are associated with variables and how objects can be changed .

Mutable Objects

Some of the mutable data types in Python are list, dictionary, set and user-defined classes

A practical example to find out the mutability of object types

>>> a = [1, 2, 3]
>>> id(a)
140226822881600
>>> a.append(4)
>>> a
[1, 2, 3, 4]
>>> id(a)
140226822881600
>>> a[2] = 5
>>> a
[1, 2, 5, 4]
>>> id(a)
140226822881600
>>>

In this example, a is a list of three integers. We can append the integer 4 to the end of this list. Running the id function before and after appending 4 shows that it is the same list both before and after. We can also use item assignment to change the value of one of the items in our list. I used item assignment to set the value of element #2 to 5. Printing id again shows that we are working on the same list.

Immutable Objects

Immutable objects are objects that cannot be changed. In Python, this would include int, float, string, user-defined classes, and more. These data types cannot be modified. This can be shown in the following example with a string:

>>> a = "Holberton"
>>> a[2] = "x"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment
>>>

We cannot change element #2 in the string “Holberton” to the letter x because strings are immutable. They cannot be changed. Let’s take a look at another example:

>>> a = "Holberton"
>>> b = "Holberton"
>>> a == b
True
>>> id(a)
140226821903600
>>> id(b)
140226821903600
>>>

In this example, strings a and b are both set to Holberton. Because strings are immutable in Python, they will both point to the same space in memory storing this string. We can test if two strings have the same value using the double equal sign, ==. We can test if two strings refer to the same object using the is operator. This tells us that a and b both refer to the same object. This can also be verified by running the id() function on both strings. Because the same string has two different names, the string is said to be aliased.

Preallocation in Python

In Python, upon startup, Python3 keeps an array of integer objects, from -5 to 256. For example, for the int object, marcos called NSMALLPOSINTS and NSMALLNEGINTS are used:

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS 257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS 5
#endif
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
/* References to small integers are saved in this array so that they
can be shared.
The integers that are saved are those in the range
-NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
*/
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
#endif
#ifdef COUNT_ALLOCS
Py_ssize_t quick_int_allocs;
Py_ssize_t quick_neg_int_allocs;
#endif

What does this mean? This means that when you create an int from the range of -5 and 256, you are actually referencing to the existing object.

How objects are passed to Functions

Its important for us to know difference between mutable and immutable types and how they are treated when passed onto functions .Memory efficiency is highly affected when the proper objects are used.

For example if a mutable object is called by reference in a function, it can change the original variable itself. Hence to avoid this, the original variable needs to be copied to another variable. Immutable objects can be called by reference because its value cannot be changed anyways. Let’s look at an example with a function that increments an int, an immutable object:

>>> def increment(a):
... a += 1
... print("id of a:" , id(a))
... return a
...
>>> x = 1
>>> y = increment(x)
id of a: 9788640
>>> print("x: ", x)
x: 1
>>> print("y: ", y)
y: 2
>>> print("id of x: ", id(x))
id of x: 9788608
>>> print("id of y: ", id(y))
id of y: 9788640
>>>

The function increment() takes an integer and increments it. It will also print out the id of the argument. We store the value 1 in the variable x and pass x as an argument to the increment function. Afterwards, we print the values and addresses of both x and y. We can see here that y holds the value 2, which is what we expect from a function that increments the number 1. Notice that the id of y matches the id of a inside the increment function. This is because both y and a are variables pointing at the same integer object.

Let’s look at another example using a mutable data type, a list:

>>> def add(argument):
... argument.append(4)
...
>>> list1 = [1, 2, 3]
>>> list2 = list1
>>> print("list1: ", list1)
list1: [1, 2, 3]
>>> print("list1 id: ", id(list1))
list1 id: 140226822881600
>>> print("list2: ", list2)
list2: [1, 2, 3]
>>> print("list2 id: ", id(list2))
list2 id: 140226822881600
>>> add(list1)
>>> print("list1: ", list1)
list1: [1, 2, 3, 4]
>>> print("list1 id: ", id(list1))
list1 id: 140226822881600
>>> print("list2: ", list2)
list2: [1, 2, 3, 4]
>>> print("list2 id: ", id(list2))
list2 id: 140226822881600
>>>

Mutable data types like lists and dicts are also passed by reference. If the value of a mutable data type is changed inside a function, the value is also changed in the caller. This is true regardless of the name of the argument.

Mutable data types like lists and dicts are also passed by reference. If the value of a mutable data type is changed inside a function, the value is also changed in the caller. This is true regardless of the name of the argument.

In this example, we created a list object called list1 and assigned the same object to the variable list2. Both list1 and list2 point to the same memory where the actual list object [1, 2, 3] is stored. We pass the list1 variable as an argument to the function add(). In this function, we append the list1 object element through the argument simply called argument. The actual object list1 is changed when we change the value in the function. The value of list2 also changes when this function is called. This is because the list1 and list2 variables both point to the same list object. The id of list1 and list2 do not change because lists are mutable and can therefore be changed. Therefore, changing a list object modifies the original object value and doesn’t create a new object.

Variables are handled completely differently in C and Python at the most fundamental level. It’s important to understand the differences to avoid confusion.