TechSchool
Home / Python3 / Mutable vs Immutable in Python

Mutable vs Immutable in Python

This page answers how mutable and immutable objects work in Python. It shows the common bugs that come from shared objects and gives the fix for each bug.

Immutable and mutable types

An immutable object cannot change after you create it. The immutable built-in types are int, float, str, bytes, tuple, frozenset, bool, and None.

A mutable object can change in place. The mutable built-in types are list, dict, set, and bytearray.

Assignment binds a name to an object. It does not copy the object. Mutation changes the object in place, and all names that point to that object see the change.

Identity and equality

The function id() returns the identity of an object. The operator is compares identity. The operator == compares value.

Two separate lists can hold the same values. The == test returns True, and the is test returns False.

Use == to compare values. Use is only for singletons such as None.

a = [1, 2]
b = [1, 2]
print(a == b)
print(a is b)
print(id(a) == id(b))

A tuple can hold a mutable object

A tuple is immutable, but it can hold a list. The tuple cannot change, but the list inside it can change.

The code below shows this trap. The list changes, and the tuple shows the new value.

The tuple still points to the same list object. The identity of the tuple does not change.

inner = [1, 2]
t = (inner, 3)
inner.append(4)
print(t)
print(t[0] is inner)

The mutable default argument bug

A default argument is evaluated one time, when Python defines the function. A list default is shared by all calls.

The first function below adds to the same list on each call. The second function uses None as a sentinel and makes a new list on each call.

Use None as the default value for a mutable argument. Then create the object inside the function.

def add_bad(item, box=[]):
    box.append(item)
    return box

def add_good(item, box=None):
    if box is None:
        box = []
    box.append(item)
    return box

print(add_bad(1), add_bad(2))
print(add_good(1), add_good(2))

Dict keys must be hashable

A dict key must be hashable. A hashable object has a stable hash value and does not change.

A list is not hashable, so Python raises a TypeError when you use a list as a key. A tuple of immutable values is a valid key.

Use a tuple as the key when you need a compound key. Convert the list to a tuple first.

d = {}
try:
    d[[1, 2]] = "x"
except TypeError as err:
    print(type(err).__name__)
d[(1, 2)] = "x"
print(d)

Shallow copy and deep copy

The function copy.copy makes a shallow copy. The new list holds the same inner objects as the old list.

The function copy.deepcopy makes a deep copy. It copies the inner objects too.

The code below shows the nested-list trap. A change to the inner list of the shallow copy also changes the original.

import copy

src = [[1, 2], [3, 4]]
shallow = copy.copy(src)
deep = copy.deepcopy(src)
shallow[0].append(9)
print(src)
print(deep)

When a function changes the caller's list

A function receives a reference to the same list object. If the function calls append or sort, the caller sees the change.

This behavior is useful when the function must update the list. It is a bug when the caller expects the list to stay the same.

Make a copy inside the function when the caller owns the list. Or return a new list and do not change the input.

Common questions

Is a tuple always immutable?

No. A tuple cannot change its own items, but it can hold a mutable object such as a list. That inner object can still change.

Why is a string immutable in Python?

A string is immutable so it can be shared and hashed safely. This makes strings valid dict keys and safe to reuse.

How do I pass a list to a function without changing it?

Pass a copy such as list(my_list) or copy.deepcopy(my_list). Then the function changes the copy, not the caller's list.

Continue learning
Python3 class Python3 module Python3 iterator generator