datarekha
Python Easy Asked at AmazonAsked at MicrosoftAsked at Google

Which Python built-in types are mutable and which are immutable, and why does it matter?

The short answer

Immutable types — int, float, bool, str, bytes, tuple, frozenset — cannot be changed after creation; operations return new objects. Mutable types — list, dict, set, bytearray — can be changed in place. Mutability determines hashability (only immutables can be dict keys/set members), function side-effect behaviour, and thread-safety considerations.

How to think about it

Mutability is a gateway question — answer it well and you’ve pre-answered a half-dozen follow-ups: “why can’t a list be a dict key?”, “why did my function change the caller’s list?”, “why doesn’t x = x + 1 mutate x?”. A strong answer lays out the table, separates rebinding from mutation, and shows the function side-effect that follows from it.

TypeMutableHashableNotes
int, float, boolNoYessmall ints cached by CPython
strNoYesmany literals interned
tupleNoYes (if contents are)immutable one level deep
frozensetNoYesthe immutable set
bytesNoYes
listYesNo
dictYesNokeys must be hashable
setYesNoelements must be hashable
bytearrayYesNothe mutable bytes

Rebinding is not mutation

The single most common confusion: reassigning a name doesn’t change the object it pointed at — it just points the name somewhere new.

x = 5
x = 6          # rebinding — x now names int 6; the int 5 is untouched

lst = [1, 2]
lst.append(3)  # mutation — the same list object is changed in place
lst = [1, 2]   # rebinding — lst now names a brand-new list

A worked example

# 1. Immutable: x + 1 builds a NEW int, so identity changes
x = 42
original_id = id(x)
x = x + 1
print(f"x after +1: {x}, same object? {id(x) == original_id}")

# 2. Mutable: append changes the SAME list, identity is unchanged
lst = [1, 2]
original_id = id(lst)
lst.append(3)
print(f"lst after append: {lst}, same object? {id(lst) == original_id}")

# 3. The practical consequence — function side effects
def double(n):
    n *= 2          # rebinds the local name; caller is untouched
def append_zero(seq):
    seq.append(0)   # mutates the SAME object the caller passed in

x = 5
double(x)
print(f"x after double(): {x}")

nums = [1, 2]
append_zero(nums)
print(f"nums after append_zero(): {nums}")

# 4. Hashability — only immutables can be keys or set members
lookup = {frozenset({1, 2}): "pair", frozenset({3}): "single"}
print("frozenset as dict key:", lookup[frozenset({1, 2})])

# 5. A tuple is immutable only one level deep
t = ([1, 2], 3)
t[0].append(99)               # the LIST inside the tuple is still mutable
print("Tuple after inner mutation:", t)
try:
    hash(t)                   # ...which is why hashing it fails
except TypeError as e:
    print("Hash error:", e)
x after +1: 43, same object? False
lst after append: [1, 2, 3], same object? True
x after double(): 5
nums after append_zero(): [1, 2, 0]
frozenset as dict key: pair
Tuple after inner mutation: ([1, 2, 99], 3)
Hash error: unhashable type: 'list'

Two results carry the whole lesson. double(x) left x at 5 — inside the function n *= 2 just rebound a local name — while append_zero(nums) reached into the caller’s list and changed it to [1, 2, 0]. That difference — mutable passed by reference versus immutable rebound locally — is the source of countless real bugs.

Why it matters

Hashability. Only immutable objects can be dict keys or set members, because their hash must never change. A mutable key could be altered after insertion and the hash table would lose track of it. Reach for frozenset when you need a set that can itself be a key or an element.

Function side effects. Hand a list to a function and it can mutate it in place; the caller sees every change. Hand an int or str and the function can only rebind its local name — the caller’s variable is safe.

Learn it properly Variables & Types

Keep practising

All Python questions

Explore further

Skip to content