Mutable in Python: objects that can be changed in place

A mutable object changes in place, without a new object being created. That is what explains the copies that turn out not to be copies.
5 min read
Believemy logo

Definition

You assign a list to a new variable, you change that new variable, and the original list changes too, even though you never touched it directly. This surprise has a precise cause: some objects change in place, others do not.

A mutable object is one that Python allows to change without building a new one. The list is the most common example: after an append, the very same object has grown, rather than a copy taking its place.

PYTHON
colours = ["red", "green"]
before = id(colours)

colours.append("blue")
print(colours, id(colours) == before)

# ['red', 'green', 'blue'] True

The opposite is called immutable: a tuple or a string never changes, any operation that seems to change one actually builds a brand new object. The distinction decides what happens as soon as two names point at the same object.


What is mutable, and what is not

A natural question follows: which types behave like the list, and which like the tuple? The table below answers type by type.

TypeChangeable in place
listYes, through append, insert or an in-place sort
dictYes, by adding, updating or removing a key
setYes, through add and discard
bytearrayYes, one byte at a time
An instance of your classYes by default, unless you take an explicit measure
int, float, bool, str, tupleNo, under no circumstances
frozensetNo, it is the frozen version of the set

One clue helps remember it without learning the table by heart: methods that return nothing worked in place, ones that return a result built a new one. colours.sort() hands back no value, since it sorted the list itself, whereas sorted(colours) builds a new one that needs capturing.


Two names for one object

An assignment such as backup = basket copies nothing: it creates a second name pointing at the same object, which the is test confirms. On an immutable object this never shows. On a mutable one, everything done through one name becomes visible through the other, since there was only ever one object.

PYTHON
basket = ["bread"]
backup = basket              # the same object, not a copy

basket.append("milk")
print(backup)                # ['bread', 'milk']

A copy has to be asked for explicitly: list(basket), a full slicing written basket[:], or copy.copy. These three forms produce a shallow copy: the outer list is new, but the objects stored inside stay shared. For a list of lists, only copy.deepcopy goes all the way down.

The diagram below shows it: two names on one object, a third one on a copy.

Two names on one object, a third one on a copy Three Python names on the left. The lines from basket and backup meet at a single point and reach one object only, the list with id 4402, where the added item "milk" stands out in colour. The name copy leads to a separate object, id 9187, still holding a single item. The names After basket.append("milk") list (id 4402) "bread" "milk" both names see it list (id 9187) "bread" the copy stays unchanged basket basket = ["bread"] backup backup = basket copy copy = basket[:]

Try it yourself with the demonstration below.

Change it through one name, watch it change through the other
original['a', 'b', 'c']same object
alias['a', 'b', 'c']same object
copy['a', 'b', 'c']separate object

At the start all three names show the same content. Nothing hints at which one shares its memory with the others.

A test with is answers the question at once: original is alias gives True, original is copy gives False. It is the first diagnostic reflex when data changes while nothing seems to touch it.


The three traps that cost the most

The first is multiplying a list that itself holds lists. It repeats the reference, not the content.

PYTHON
grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid)

# [[1, 0, 0], [1, 0, 0], [1, 0, 0]]

The three rows are the same list looked at three times. The correct form goes through a list comprehension, [[0] * 3 for _ in range(3)], which rebuilds a fresh row on every turn.

The second is the mutable default value of a function, evaluated once at definition time and not on every call: the same list then serves every call and grows without end.

Warning

Writing def add(item, basket=[]): looks harmless, but that default basket is created only once and shared by every call. The convention is to write basket=None, then build the list inside the body of the function, None marking the absence.

The third is the mutable attribute declared at class level. A list written there belongs to the class rather than to the objects: every instance shares it, and an addition made for one shows up on the others. Its place is inside __init__.


Why a mutable cannot be a key

Why can a list not be a dictionary key, when an identical tuple can? A dictionary stores its items according to a hash worked out from the value. If that value changed afterwards, the hash would turn wrong and the item unreachable. Python cuts it short and refuses the operation.

PYTHON
index = {}
index[["a", "b"]] = 1

# TypeError: unhashable type: 'list'

The TypeError here is a protection rather than a whim. The cure is to freeze the key: a tuple instead of the list, a frozenset instead of the set. It is the best answer to anyone asking why the tuple exists when the list seems to cover everything.


Frequently asked questions

Question

How can an object be checked as mutable?

By trying hash(object) in a console: whatever refuses to be hashed is mutable, and whatever accepts almost never is. The second clue is the presence of methods that return no value, a sign that they worked directly on the object.

Question

Is a tuple holding a list changeable?

The tuple stays immutable: none of its slots can receive anything else. The list stored inside, however, continues living its own life and can change. That tuple loses the right to serve as a dictionary key, since its hash would no longer be reliable.

Question

How can objects of your own be made unchangeable?

The simplest route is a dataclass declared with frozen=True, which refuses any attribute reassignment after construction.

Related terms

Discover our python glossary

Browse the terms and definitions most commonly used in development with Python.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.