Immutable objects

By Martin McBride, 2018-03-26
Tags: object mutability immutable object
Categories: python language intermediate python


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.

See also

If you found this article useful, you might be interested in the book NumPy Recipes or other books by the same author.

Join the PythonInformer Newsletter

Sign up using this form to receive an email when new content is added:

Popular tags

2d arrays abstract data type alignment and angle animation arc array arrays bar chart bar style behavioural pattern bezier curve built-in function callable object chain circle classes clipping close closure cmyk colour combinations comparison operator comprehension context context manager conversion count creational pattern data science data types decorator design pattern device space dictionary drawing duck typing efficiency ellipse else encryption enumerate fill filter font font style for loop formula function function composition function plot functools game development generativepy tutorial generator geometry gif global variable gradient greyscale higher order function hsl html image image processing imagesurface immutable object in operator index inner function input installing iter iterable iterator itertools join l system lambda function latex len lerp line line plot line style linear gradient linspace list list comprehension logical operator lru_cache magic method mandelbrot mandelbrot set map marker style matplotlib monad mutability named parameter numeric python numpy object open operator optimisation optional parameter or pandas partial application path pattern permutations pie chart pil pillow polygon pong positional parameter print product programming paradigms programming techniques pure function python standard library radial gradient range recipes rectangle recursion reduce regular polygon repeat rgb rotation roundrect scaling scatter plot scipy sector segment sequence setup shape singleton slice slicing sound spirograph sprite square str stream string stroke structural pattern subpath symmetric encryption template tex text text metrics tinkerbell fractal transform translation transparency triangle truthy value tuple turtle unpacking user space vectorisation webserver website while loop zip zip_longest