Mutable, immutable, and what does hashable have to do with it?
In Python, the concepts of mutable, immutable, hashable, and unhashable often come up together. Especially when talking about lists, tuples, dictionaries, and sets. At first, it's easy to draw a rather simple connection between them: mutable objects cannot be hashed, while immutable ones can.
In most everyday examples, this does seem to be true. A list is mutable and unhashable, while a string is immutable and hashable. But if we dig a little deeper, it turns out that these are actually different properties of an object.
Let's start with mutable and immutable.
Mutable means an object can be changed after it has been created. The most obvious example is a regular list:
numbers = [1, 2, 3]
numbers.append(4)
print(numbers)
# [1, 2, 3, 4]
Here, the list that originally contained three elements now contains four. And importantly, it is still the very same object.
We can even look at its id:
numbers = [1, 2, 3]
print(id(numbers))
numbers.append(4)
print(id(numbers))
In both cases, the id will be the same. We took an existing object and changed it.
Strings work differently. Strings in Python are immutable, which means that once they have been created, the object itself cannot be changed.
For example, this will not work:
name = "cat"
name[0] = "b"
Python will raise a TypeError.
At the same time, nothing prevents us from writing:
name = "cat"
name = "bat"
At first glance, it might seem that we changed the string after all. But we didn't. The variable `name` used to refer to the string "cat", and after the second assignment it refers to another object — the string "bat".
This is an important distinction. Immutable does not mean that a variable cannot be changed. A variable is not the object itself in the first place. Roughly speaking, it is a name that refers to an object. You can change what the name refers to, but you cannot change the immutable object itself.
The same thing happens with numbers:
x = 10
x += 1
The number 10 did not turn into 11. As a result of the operation, `x` simply started referring to a different value.
Common mutable built-in types include list, dict, and set. Immutable ones include int, float, bool, str, bytes, tuple, and frozenset.
And somewhere around this point, another question usually comes up: why can't a list be used as a dictionary key, while a tuple can?
data = {
(10, 20): "point"
}
This is a perfectly valid dictionary.
But this one:
data = {
[10, 20]: "point"
}
will not even be created:
TypeError: unhashable type: 'list'
To understand why, we need to look at what hashable means.
Python has a built-in function called hash():
hash(42)
hash("hello")
hash((1, 2, 3))
All of these calls work and return some integer. The actual number usually does not matter to us. What matters is that Python can obtain a hash for an object and use it in hash tables, which are used internally by dictionaries and sets, among other things.
For example, when we write:
users = {
"alice": 25,
"bob": 31,
}
Python does not go through every dictionary key from beginning to end each time we access users["alice"]. The key's hash helps Python quickly determine where to look for the corresponding entry.
This leads to an important requirement: an object's hash must remain stable while the object is being used in this way.
And this is where it becomes clear why a list would cause problems.
Let's imagine for a moment that Python allowed us to do this:
key = [1, 2]
data = {
key: "hello"
}
Python would calculate the hash of [1, 2] and use it to place the entry in a particular location inside the dictionary.
And then we could do this:
key.append(3)
Now our key is [1, 2, 3].
If the hash depends on the contents of the list, it would have to change as well. This creates a strange situation: the entry was placed in the dictionary using one hash, but now we would have to look for it using another.
That is why a regular list simply cannot be hashed:
hash([1, 2, 3])
will result in an error:
TypeError: unhashable type: 'list'
The same applies to dict and set. They are all mutable and therefore are not suitable for regular content-based hashing.
This is where the useful association comes from:
list → mutable → unhashable
dict → mutable → unhashable
set → mutable → unhashable
str → immutable → hashable
int → immutable → hashable
bytes → immutable → hashable
But it is still better not to treat this as a strict rule.
A good example is tuple.
A tuple cannot be changed:
point = (10, 20)
point[0] = 100
This will raise an error. So it seems reasonable to expect that a tuple can be hashed:
point = (10, 20)
print(hash(point))
And indeed, it can.
Because of this, tuples are convenient to use as dictionary keys. Coordinates are a good example:
places = {
(40.7128, -74.0060): "New York",
(51.5074, -0.1278): "London",
}
But now let's put a list inside a tuple:
value = (1, 2, [3, 4])
The tuple itself is still immutable. We cannot write:
value[0] = 100
But nothing prevents us from changing the list inside it:
value[2].append(5)
print(value)
# (1, 2, [3, 4, 5])
Now let's try:
hash(value)
and we get a TypeError.
So a tuple itself is immutable, but that does not guarantee that a particular tuple is hashable. Its elements must also be hashable.
There is an interesting detail here: the phrase "a tuple cannot be changed" is sometimes taken too literally. You cannot change which objects the tuple's positions refer to. But if a tuple contains a mutable object, such as a list, that object itself can still be changed.
There is a similar story with sets. A regular set can be changed:
numbers = {1, 2, 3}
numbers.add(4)
Therefore, a set itself is unhashable and cannot be an element of another set.
But Python also has frozenset:
numbers = frozenset({1, 2, 3})
print(hash(numbers))
This is an immutable set, and it can be hashed. Therefore, unlike a regular set, a frozenset can be used as a dictionary key or as an element of another set.
There is one more rule that helps explain the idea of hashability. If two hashable objects are equal:
a == b
then their hashes must also be equal:
hash(a) == hash(b)
In other words, if a == b, their hashes must be equal as well.
The reverse is not true. Two different objects can theoretically have the same hash. This is called a hash collision, and Python knows how to handle such situations.
In practice, all of this becomes much easier to understand if we don't try to treat mutable and hashable as the same concept.
Mutable and immutable describe whether an object's state can be changed after the object has been created.
Hashable and unhashable describe whether an object is suitable for use in Python's hash-based data structures. In practice, this primarily means whether it can be used as a dict key or as an element of a set.
For standard types, this gives us a fairly familiar picture. Strings, numbers, and suitable tuples can be used as dictionary keys. Lists, dictionaries, and sets cannot.
data = {
"name": "Alice", # str — allowed
42: "answer", # int — allowed
(10, 20): "point", # tuple — allowed
}
But this:
data = {
[10, 20]: "point"
}
will not work.
And this is probably one of those cases where understanding the reason is more useful than memorizing a table of types. If you remember why a dictionary needs a hash in the first place and why that hash has to remain stable, the behavior of list, tuple, set, and frozenset stops looking like a collection of arbitrary rules.