Immutable objects

Some of Python’s most common objects are immutable - in particular tuples and strings. In this article we will look at what this means, and the pros and cons on immtability.

What is immutability?

When we say that a Python object is immutable, we simple mean that its value cannot be changed after it is first created. Tuples are a good example - they are very similar to lists, but they are immutable.

If you look at the methods of a list, you will see that there are some which change the list, some which don’t:

k = [1, 2, 3]

#These are examples of methods which do NOT change the list
#These same methods exist for tuples too.
k.index(2)
k.count(0)

#These are examples of methods which change the list
#They methods don't exist for tuples
k.append(4)
k.reverse()

A tuple has exactly the same methods as a list, except that the methods which change a list simply don’t exist for a tuple. You can’t change the value of a tuple because the methods to do it aren’t provided.

Also of course, you can only use the [] operator to read values of a tuple, not set them:

t = (10, 11, 12)
a = t[2]         #That is legal
t[2] = 20        #Error, you cannot set values in a tuple

Why have immutable objects?

Consider the following code:

a = [1, 2, 3]
b = a
b.reverse()
print(a)      #[3, 2, 1]

Here, a and b both reference the same list. We call this aliasing. If we reverse b, then it automatically affects a too, because they are just different names for the same object. Imagine this happening in a very complex program, where a and b are being used in different sections of code - it can become very difficult to keep track what is going on.

Look what happens if we use tuples instead:

a = (1, 2, 3)
b = a
b = reversed(b)
print(a)      #(1, 2, 3)

Tuples don’t have a reverse function, because reverse affects the ordering and that isn’t allowed for immutable objects.

So instead, we are forced touse the built-in function reversed which creates a brand new sequence which is the reverse of the original. So now, b references a brand new sequence, while a still references the original sequence. Nothing you do with variable b can possibly affect the value of a.

Lists can also use the reversed function, and if you are careful in your code you can avoid ever using reverse. But it is easy to forget, and accidentally change a list when you shouldn’t. With a tuple it is impossible to mak ethis mistake.

Here is another example, a problem that can occur is you pass a list into a function:

def evil_print(k):
    print(k)
    k[1] = 0

a = [1, 2, 3]
evil_print(a)   # [1, 2, 3]
print(a)        # [1, 0, 3]

Here the evil_print function prints the list k. But it has a sting in its tail - it also randomly sets k[1] to zero. This is just an example, a real function probably wouldn’t do something so pointlessly destructive, but it could something just as bad by accident.

The problem is that within the function call, k is an alias of the variable a. Anything you do to k will happen to a.

If you make your code use tuples, the problem can’t occur (you would get a runtime error when trying to set k[1], but that is a lot better than some random error later on when the value of a has inexplicably changed).

Immutables are so much safer, because they cannot be accidentally changed by other code. So you might ask the opposite question:

Why have mutable objects?

In a word, efficiency.

Unfortunately, there is one problem with tuples compared to lists. Whenever you make any changes to a tuple, even just changing a single element, you must make a brand new copy of the entire tuple. Here is how we might change a value if a tuple:

t = (1, 2, 3, 4, 5)
#Change element 2 to 10
t = t[:2] + (10,) + t[3:]

Here we are using slices to create copies of the start and end parts of the original tuple, which we then concatenate with the new element in the middle. We end up making a complete copy of the original.

Imagine that you had a very long tuple, which you needed to change frequently - your code would spend all of its time making copies of the tuple, and would run very slowly. In these cases it is usually better to use a list, and be careful to avoid aliasing situations in the code.

What about numbers?

This is an issue which people sometimes find a little confusing:

x = 4
y = x

Now we know that 4 is an integer object. And when we execute y = x it means that y now references the same object as x:

numbers-1

So what happens if we do this:

x = 3

Does y now equal 3?

Well, fortunately, it still equals 4. The reason is that numbers are also immutable. We can’t change the value of the integer object which x references, Python must create a brand new integer object with value 3, and set x to reference the new object. But y still references the old object with value 4:

numbers-2

Immutability is not passed on

When we say that a tuple is immutable, it is important to understand exactly what we mean. For example:

a = [1, 2, 3]
b = [4, 5, 6]
c = [7, 8, 9]
t = (a, b, c)
print(t)

As you might expect, this gives:

([1, 2, 3], [4, 5, 6], [7, 8, 9])

But then try this:

a[1] = 55
print(t)

Which gives:

([1, 55, 3], [4, 5, 6], [7, 8, 9])

Hold on, didn’t we just change a tuple? Well, no, as this diagram shows:

tuple-1

The tuple contains 3 references to list objects. Changing the content of one of the lists doesn’t change the tuple - it still contains the same 3 references to the same lists.