Mutability and immutability

Published

2023-08-01

Mutability and immutability

Mutability and immutability are properties of certain classes of object. For example, these are immutable—once created they cannot be changed:

  • numeric types (int and float)
  • Booleans
  • strings
  • tuples

However, lists are mutable. Later, we’ll see another mutable data structure, the dictionary.

Immutable objects

You may ask what’s been happening in cases like this:

>>> x = 75021
>>> x
75021
>>> x = 61995
>>> x
61995

Aren’t we changing the value of x? While we might speak this way casually, what’s really going on here is that we’re creating a new int, x.

Here’s how we can demonstrate this—using Python’s built-in function id().1

>>> x = 75021
>>> id(x)
4386586928
>>> x = 61995
>>> id(x)
4386586960

See? The IDs have changed.

The IDs you’ll see if you try this on your computer will no doubt be different. But you get the idea: different IDs mean we have two different objects!

Same goes for strings, another immutable type.

>>> s = 'Pharoah Sanders'  # who passed away the day I wrote this
>>> id(s)
4412581232
>>> s = 'Sal Nistico'
>>> id(s)
4412574640

Same goes for tuples, another immutable type.

>>> t = ('a', 'b', 'c')
>>> id(t)
4412469504
>>> t[0] = 'z'   # Try to change an element of t...
Traceback (most recent call last):
  File "/Library/.../code.py", line 90, in runcode
    exec(code, self.locals)
  File "<input>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> id(t)   # still the same object
4412469504
>>> t = ('z', 'y', 'x')
>>> id(t)
4412558784

Mutable objects

Now let’s see what happens in the case of a list. Lists are mutable.

>>> parts = ['rim', 'hub', 'spokes']
>>> id(parts)
4412569472
>>> parts.append('inner tube')
>>> parts
['rim', 'hub', 'spokes', 'inner tube']
>>> id(parts)
4412569472
>>> parts.pop(0)
'rim'
>>> parts
['hub', 'spokes', 'inner tube']
>>> id(parts)
4412569472

See? We make changes to the list and the ID remains unchanged. It’s the same object throughout!

Variables, names, and mutability

Assignment in Python is all about names, and it’s important to understand that when we make assignments we are not copying values from one variable to another. This becomes most clear when we examine the behavior with respect to mutable objects (for example, lists):

>>> lst_a = [1, 2, 3, 4, 5]
>>> lst_b = lst_a

Now let’s change lst_a.

>>> lst_a.append(6)
>>> lst_b
[1, 2, 3, 4, 5, 6]

See? lst_b isn’t a copy of lst_a, it’s a different name for the same object!

If a mutable value has more than one name, if we affect some change in the value via one name, all the other names still refer to the mutated value.

Now, what do you think about this example:

>>> lst_a = [1, 2, 3, 4, 5]
>>> lst_b = [1, 2, 3, 4, 5]
>>> lst_a.append(6)

Are lst_a and lst_b different names for the same object? Or do they refer to different objects?

>>> lst_a
[1, 2, 3, 4, 5, 6]
>>> lst_b
[1, 2, 3, 4, 5]

lst_a and lst_b are names for different objects! Now, does this mean that assignment works differently for mutable and immutable objects? Not at all.

Then why, you may ask, when we assign 1 to x and 1 to y do both names refer to the same value, whereas when we assign [1, 2, 3, 4, 5] to lst_a and [1, 2, 3, 4, 5] to lst_b we have two different lists?

Let’s say you and a friend wrote down lists of the three greatest baseball teams of all time. Furthermore, let’s say your lists were identical…

Trigger warning: opinions about MLB teams follow!

>>> my_list = ['Cubs', 'Tigers', 'Dodgers']
>>> janes_list = ['Cubs', 'Tigers', 'Dodgers']

Now, my list is my list, and Jane’s list is Jane’s list. These are two different lists.

Let’s say that the Dodgers fell out of favor with Jane, and she replaced them with the Cardinals (abhorrent, yes, I know).

>>> janes_list.pop()
'Dodgers'
>>> janes_list.append('Cardinals')
>>> janes_list
['Cubs', 'Tigers', 'Cardinals']
>>> my_list 
['Cubs', 'Tigers', 'Dodgers']

That makes sense, right? Even though the lists started with identical elements, they’re still two different lists and mutating one does not mutate the other.

But be aware that we can give two different names to the same mutable object (as shown above).

>>> lst_a = [1, 2, 3, 4, 5]
>>> lst_b = lst_a
>>> lst_a.append(6)
>>> lst_a
[1, 2, 3, 4, 5, 6]
>>> lst_b
[1, 2, 3, 4, 5, 6]

This latter case is relevant when we pass a list to a function. We may think we’re making a copy of the list, when in fact, we’re only giving it another name. This can result in unexpected behavior—we think we’re modifying a copy of a list, when we’re actually modifying the list under another name!

Original author: Clayton Cafiero < [given name] DOT [surname] AT uvm DOT edu >

No generative AI was used in producing this material. This was written the old-fashioned way.

This material is for free use under either the GNU Free Documentation License or the Creative Commons Attribution-ShareAlike 3.0 United States License (take your pick).

Footnotes

  1. While using id() is fine for tinkering around in the Python shell, this is the only place it should be used. Never include id() in any programs you write. The Python documentation states that id() returns “the ‘identity’ of an object. This is an integer which is guaranteed to be unique and constant for this object during its lifetime. Two objects with non-overlapping lifetimes may have the same id() value.” So please keep this in mind.↩︎