Python3: Mutable, Immutable… everything is object!

Object Oriented Programming

Object Oriented Programming (OOP) is a programming paradigm in which the relevant real world concepts for solving a problem are modeled through classes and objects; and under this concept, the programs consist of a series of interactions between these objects.

Object

To understand this paradigm we first have to understand what is a class and what is an object. An object is an entity that groups together a related state and functionality. The state of the object is defined through variables called attributes, while the functionality is modeled through functions that are known by the name of object methods.

An example of an object could be a car, in which we would have attributes such as the brand, the number of doors or the type of fuel and methods such as starting and stopping. Or any other combination of attributes and methods depending on what is relevant to our program.

Class

A class, on the other hand, is nothing more than a generic template from which to instantiate objects; template that is the one that defines what attributes and methods will have the objects of that class. This is why everything in Python is an object.

Continuing with the example: in the real world there is a set of objects that we call cars that have a set of common attributes and a common behavior, this is what we call class. However, my car is not the same as my neighbor’s car, and although they belong to the same class of objects, they are different objects.

Unlike other programming languages where the language supports objects, in Python everything is really an object, including integers, lists, and even functions.

One way to verify this is by using the built-in function , which returns True if the specified object is of the specified type, and False otherwise.

>>> isinstance(1, object)
True
>>> isinstance(False, object)
True
def my_func():
return "hello"
>>> isinstance(my_func, object)
True

As we could verify everything in Python is an object; therefore, all data in Python code is represented by objects or by relationships between objects. And the most commonly used standard composite data types to represent objects, we can see them in the following table:

Standard compound data types

These objects can be classified as:

  • Mutable Objects: because their content (or that value) can be changed at runtime.
  • Immutable Objects: because their content (or that value) cannot be changed at runtime.

Type

The simplest way to check the type of object we are working with in Python is to use the built-in function. This will allow us to see that everything can be treated in the same way, as an object -or instance- of the class to which they belong.

>>> x = 42 
>>> typex
<class 'int'>
>>> y = 24.5
>>> type(y)
<class 'float'>
>>> def f(x):
... return (x+1)
...
>>> type(f)
<class 'function'>
>>> import math
>>> type(math)
<class 'module'>

With these examples, we can verify that all objects are treated in the same way.

Identity

Another of the built-in Python functions is which returns the address of an object in memory.

>>> x = 1
>>> id(x)
10105088

We create an object with the name of and assign it the value of . Then we use to see that the object is at memory address .

This allows us to check interesting things about Python. Let’s say we create two variables in Python, one named and one named , and assign them the same value. For example here:

>>> x = "Holberton"
>>> y = "Holberton"

We can use the equality operator (==) to verify that they do indeed have the same value in Python’s eyes:

>>> x == y
True

But are these the same object in memory? In theory, there can be two very different scenarios here.

A scenario (A) in which we actually have two different objects, one with the name of x and one with the name of y, which happen to have the same value. And a scenario (B) where only one object is stored, which has two names that refer to it.

We can use the function presented above to verify this:

>>> x = "Holberton School"
>>> y = "Holberton School"
>>> x == y
True
>>> id(x)
139798528064800
>>> id(y)
139798528064872

So, as we can see, Python’s behavior matches Scenario (A) described above. Although in this example (that is, and y have the same values), they are different objects in memory. This is because , as we can explicitly verify:

>>> id(x) == id(y)
False

There is a shorter way to do the above comparison, and that is to use Python’s is operator. Checking if x is y is the same as checking , which means if and are the same object in memory:

>>> x == y
True
>>> id(x) == id(y)
False
>>> x is y
False

This allows us to see the important difference between the equality operator and the identity operator . As you can see from the example above, it is entirely possible that the two names in Python ( and ) are subject to two different objects (and therefore is ), where these two objects have the same value (so that is ).

How can we create another variable that points to the same object that points to? What is called aliasing, which is when two or more variables refer to the same object. We can simply use the assignment operator , like this:

>>> x = "Holberton School"
>>> z = x

To verify that they actually point to the same object, we can use the is operator:

Of course this means they have the same address in memory, as we can explicitly check using :

>>> id(x)
139798528064944
>>> id(z)
139798528064944

And of course they have the same value, so we also expect to return :

>>> x == z
True

Mutable and Immutable Objects

Mutable and Immutable Objects

As we indicated, in Python everything is an object, however, there is an important distinction between objects. Some objects are mutable while others are immutable.

Immutable objects

For some types in Python, once we have instantiated those types, they never change. They are immutable. For example, objects are immutable in Python. What will happen if we try to change the value of an object?

>>> x = 37598
>>> x
37598
>>> x = 37599
>>> x
37599

Well, it seems that we changed successfully. But is this really going to be so? What exactly happened under the hood here? To find out, let’s use to investigate further:

>>> x = 37598
>>> x
37598
>>> id(x)
139798529290128
>>> x = 37599
>>> x
37599
>>> id(x)
139798528694160

So we can see that by assigning , we don’t change the value of the object that had been linked to earlier. Rather, we create a new object and bind the name to it. So after assigning to by using , we had the following state:

And after using we create a new object and bind the name to this new object. The other object with the value of is no longer accessible by (or any other name in this case):

Whenever we assign a new value to a name (in the example above ) that is bound to an object, we actually change the binding of that name to another object.

The same also applies to tuples, strings (str objects), and booleans. In other words, int (and other types of numbers such as float), tuple, bool, and str are immutable objects. Let’s test this hypothesis. What happens if we create a tuple object and then give it a different value?

>>> my_tuple = (1, 2, 3)
>>> id(my_tuple)
139798528063096
>>> my_tuple = (3, 4, 5)
>>> id(my_tuple)
139798528064608

Like an int object, we can see that our assignment actually changed the object that the name is bound to. What happens if we try to change one of the elements of ?

>>> my_tuple[0] = 'a new value'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

As we can see, Python doesn’t allow us to modify the content of the object, because it is immutable.

The value of objects of immutable type cannot change without changing the identity of the object. Therefore, whenever we change the value that a variable refers to, we are actually changing the reference object of that variable to a new one. Python keeps an internal counter on how many references an object has. Once the counter reaches zero, which means that no reference is made to the object, the garbage collector in Python removes the object, thus freeing up memory.

Mutable Objects

Some types in Python can be modified after creation and are called mutables. For example, we know that we can modify the content of a object:

>>> my_list = [1, 2, 3]
>>> my_list[0] = 'new value'
>>> my_list
['new value', 2, 3]

Does that mean we actually create a new object by assigning a new value to the first element of ? Again we can use id to check:

>>> my_list = [1, 2, 3]
>>> id(my_list)
139798488481416
>>> my_list
[1, 2, 3]
>>> my_list[0] = 'new value'
>>> id(my_list)
139798488481416
>>> my_list
['new value', 2, 3]

Thus, our first assignment creates an object at address , with values of , , and :

Then we modify the first element of this list object using , that is, without creating a new list object:

Now, let’s create two names, and both linked to the same list object. We can verify that by using is, or by explicitly checking its ids:

>>> x = y = [1, 2]
>>> x is y
True
>>> id(x)
139798488480520
>>> id(y)
139798488480520
>>> id(x) == id(y)
True

What happens now if we use ? That is, if we add a new element to the object with the name of ?

Will change? and ? Well, as we already know, they are basically two names of the same object:

Since this object has changed, when we check their names we can see the new value:

>>> x.append(3)
>>> x
[1, 2, 3]
>>> y
[1, 2, 3]

Note that and have the same as before, as they are still bound to the same object:

>>> id(x)
139798488480520
>>> id(y)
139798488480520

Why is it important and how does Python handle mutable and immutable objects?

It is important to know how Python handles mutable and immutable objects to avoid errors or modifying data when that is not the wish. Let’s look at an example.

Next, we define a list (mutable object) and tuple (immutable object) . The list includes a tuple, and the tuple includes a list:

>>> my_list = [(1, 1), 2, 3]
>>> my_tuple = ([1, 1], 2, 3)
>>> type(my_list)
<class 'list'>
>>> type(my_list[0])
<class 'tuple'>
>>> type(my_tuple)
<class 'tuple'>
>>> type(my_tuple[0])
<class 'list'>

So far so good. Now what will happen when we try to execute each of the following statements?

>>> my_list[0][0] = 'Changed!'    (1)
>>> my_tuple[0][0] = 'Changed!' (2)

In statement (1), what we are trying to do is change the first my_list element, that is, a tuple. Since a tuple is immutable, this attempt is bound to fail:

>>> my_list[0][0] = 'Changed!'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

Note that what we were trying to do is not change the list, but change the content of its first element. Let’s consider statement (2). In this case, we are accessing the first element of , which happens to be a list, and we modify it. Let’s review this case further and look at the addresses of these elements:

>>> my_tuple = ([1, 1], 2, 3)
>>> id(my_tuple)
139798528063096
>>> type(my_tuple[0])
<class 'list'>
>>> id(my_tuple[0])
139798520723336

When we change , we don’t really change at all! In fact, after the change, the first element of will still be the object whose memory address is . However, we change the value of that object:

>>> my_tuple[0][0] = 'Changed!'
>>> id(my_tuple)
139798528063096
>>> id(my_tuple[0])
139798520723336
>>> my_tuple
(['Changed!', 1], 2, 3)

Both and remain the same after change.

Since we only modify the value of , which is a mutable object of type list, Python allowed this operation.

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

Its important to know the 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.

>>> def increment(n):
... n += 1
...
>>> a = 9
>>> increment(a)
>>> a
9

>>> def increment(l):
... l += [4]
...
>>> l = [1, 2, 3]
>>> increment(l)
>>> l
[1, 2, 3, 4]

Preallocation in Python

Now a homework for you:
1. create two variables with values between -5 and 256 and then check if they reference to the same object.
2. Do the same but using values for the variables out of the range above.
What happened?

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

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.
This is made to avoid to create again objects that are commonly used and because in that way you can represent any ASCII character.