The tricky side of Python3: Mutable, Immutable!

Fun topic to totally understand the behaviour between them

Mutable vs Immutable

Before we will start to study the Mutable and Immutable case of Python3, we need to understand Why everything is an object in Python!

Introduction: What means “everything is an object”?

Firstly, we need to know that Python is a different language than low-level languages programming like C, and it is because Python is an Object-Oriented Programming (OOP) language.

And this is the reason why the Python programming language consists of objects. So it allows user to have their own methods and attributes without creating them each time they need it. Also, we can see that Python is built off of a data structure called the PyObject, which is what all the data types inherit from — hence why the language consists of objects.

That’s why is also important to study the behaviour of the objects in Python: case of Immutable and Mutable objects

Immutable vs Mutable, how we can work with them while we are coding?

id and type

id() — a function that returns the identity of the object. This is an integer that is unique for the given object and remains constant during its lifetime. For example, in the next example, we can see the identity of a and b, and there are different

how to use id

type() — returns the type of the object or returns a new type object based on the arguments passed. In the next example, we can see that it returns the <class ‘int’>, because finally, a is an object of int. Also, type has the property to create a dictionary if you decide to give it more objects. So, it will return a dictionary with the keys(object to evaluate) and values (type of each variable)

How to use type

And of course, id and type are objects because everything in Python is an object

Immutable objects

Immutable objects are objects whose values can’t change — examples of Immutable objects are int, string, tuple . We can see the complete list of immutable objects in the next picture:

List of Immutable and Mutable Objects

Let’s check an example of an immutable object like a string:

a = "banana"
b = "banana"

Is this object has the same value?

>>> a == b

Is this object has the same id?

>>> a is b

This is happening because strings are immutable. Python in this way optimizes resources. This is not the case with the lists. So we can say that they refer to the same thing as the next picture.

a & b refers to same thing: “banana” . this is happening when an object is immutable

But, what happens with the id() when we try to modify one string?

# Assigning two objects to the same string
>>> a = "b"
>>> b = "b"
>>> id(a), id(b)
(139873424855424, 139873424855424)# Both strings have the same id
# Adding third object 'c'
>>> c = "c"
>>> id(c)
>>> a += c
>>> print(a)
>>> id(a)

We see that id changes because now the a refers to a different string.

Mutable objects

Mutable objects are objects whose values can change — examples of mutable objects are lists, dict, set, byte array . Let’s check an example:

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

We can see that the value of a & b list is the same but the id() is different! It is because they reference to different object. We can represent better in the next image:

They reference to different object

But what happens if we want to append new items to the list?

>>> a = [1, 2]
>>> b = [1, 2]
>>> id(a)
>>> id(b)
>>> a += [3, 4]
>>> id (a)
>>> a = a + [5, 6]
>>> id (a)

We can see as the picture before that id() changes like the “way” we append new items, but it is happening because in the first example “a += a + [5,6]”. We modify the list by adding values. We can see better in the next image checking their addresses

We check that we are appending new items to the actual list, so the id() is the same

But in the next case “ a = a + [3,4]”, we call a and add [3,4], with this process we are creating a new list and says that a will refers to this new list. We can see better in the next example:

We can see that a refers to this new list with different id()

Extra point: We can also create a list and “referring” to another list. This is possible with the concept of “Aliasing”

We can see that instead of creating another list with similar values of a. we can only use the “=” and it will reference the same list.

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

So, in this case, we can say that both lists reference to the same object

This MUTABLE lists has the same reference because we use the “Aliasing”

Why does it matter and how differently does Python treat mutable and immutable objects

A mutable object can be changed after it’s created, and an immutable object. We can resume their behavior in the next picture

mutable and immutable behavior

So we can conclude with mutable, we can manipulate their value of this object and change their value. But in the opposite case, we can not change the value of an immutable object.

We can see this behaviour in the next example:

x = 'foo'
y = x
print x # foo
y += 'bar'
print x # foo

x = [1, 2, 3]
y = x
print x # [1, 2, 3]
y += [3, 2, 1]
print x # [1, 2, 3, 3, 2, 1]

def func(val):
val += 'bar'

x = 'foo'
print x # foo
print x # foo

def func(val):
val += [3, 2, 1]

x = [1, 2, 3]
print x # [1, 2, 3]

We can see that we can change the value of a mutable object only when we assign the, like parameters, but in the case of an immutable variable, not.

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

In both of the cases, the argument “pass by reference” and that is an opposite case like we see before in c language: “pass by value”.

So, with Python, we can conclude that the argument creates a reference to the object like we see in the next picture:

Let’s check for an immutable case:

>>> def increment(n):
... n += 1
>>> a = 3
>>> increment(a)
>>> print(a)
a = 3 # a is still referring to the same object

We can see that when we call the function increment with “a” like a parameter. At this time the reference of “n” to “a” is created. So, we can say that a & n is referring to object 3:

“a” and “n” refers to “3”

Because n is an integer and when we want try to do n += 1 , we can not do it, because n is immutable and the definition of it says that we are not able to modify the object’s value to 4. So instantly, n will refers to a new object: “4” like the next image:

n refers to the new object, no more to “3”

But also, it is important to say that a continues referring to object “3”

a continuing referring to 3

And this explains why the value of a doesn’t change after calling increment().

But now the question is How will be able to manipulate immutable objects by passing them to functions like parameters?

We can “modify” the immutable object by capturing the return of the function. Like the next example:

>>> def increment(n):
... n += 1
... return n
>>> a = 3
>>> a = increment(a) # the return value of increment() is captured!
>>> print(a)
a = 4 # a now refers to the new object created by the function

Fun fact!: We can also call to his kind of functions like “Pure functions”. Because it doesn’t produce side effects

Now let’s study what is happening when we pass a mutable object:

The same increment()function generates a different result when we passing a mutable object:

>>> def increment(n):
... n.append([4])
>>> L = [1, 2, 3] # mutable object (list)
>>> increment(L)
>>> print(L)
L = [1, 2, 3, 4] # we can see that a changed!

First, let’s study the references:

  • L refers to the list object that contains 3 immutable objects: integer 1, 2, 3. We can see also in a beauty graphic:
L is referring to a list object that contains 3 immutable object

When we will pass L to increment(), now n is referring to the same object as L, like the next image:

The .append() method modifies the list in place because the list is mutable!! So we can see that L and n are continuing referring to the same object like the image:

after append() L and n is continuing referring to the same object

Now we can see Immutable and mutable with another eyes!!!

Finally, What means Small Integer Caching?

Python caches small integers, which are integers between -5 and 256. These numbers are used so frequently that it’s better for performance to already have these objects available. That’s why these numbers (objects) are preallocated in Python. So, when you want to refer to one of them, you’ll be referring to an object that already exists.

We can found these values like macros in the int object:

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];

So that’s it! I hope that you can enjoy these topics like me! and obviously, happy coding :)!

Software engineer in progress