Mutable vs immutable in Python comes down to one question: can the object change after it’s created? Lists, dictionaries and sets are mutable, so they change in place. Strings, ints, floats and tuples are immutable, so every “change” builds a new object.
cart = ["laptop"]
cart.append("mouse") # the same list, now longer
city = "Austin"
city = city + ", TX" # a brand-new string
I ran each snippet on Python 3.12.5, and the screenshots are the real Command Prompt output, ids and all.
It sounds academic, but it explains a whole family of bugs: a copy that isn’t a copy, a function that edits your data, and a default argument that remembers old calls.
How to see whether a Python object is mutable
id() gives every object a number that stays fixed for its lifetime. Compare it before and after a change, and you’ll see which types change in place:
# a list is changed in place: same object afterwards
cart = ["laptop"]
before = id(cart)
cart.append("mouse")
print("list :", cart, "| same object:", id(cart) == before)
# a string can't change, so + builds a new one
city = "Austin"
before = id(city)
city = city + ", TX"
print("str :", city, "| same object:", id(city) == before)
# so does an int, even with +=
count = 10
before = id(count)
count += 1
print("int :", count, "| same object:", id(count) == before)
# and a tuple
point = (30.27, -97.74)
before = id(point)
point += (149,)
print("tuple:", point, "| same object:", id(point) == before)
Output:
list : ['laptop', 'mouse'] | same object: True
str : Austin, TX | same object: False
int : 11 | same object: False
tuple: (30.27, -97.74, 149) | same object: False
Notice that count += 1 didn’t change the number 10. It created 11 and moved the name count to it, which is why the id changed.
That’s the whole idea in one sentence: with an immutable type, you can rebind the name, but you can’t edit the object.
Which Python types are mutable and which are immutable?
Here’s a quick test across the built-in types. Python refuses to hash a mutable object, which makes hash() a handy probe:
samples = {
"int": 42, "float": 19.99, "bool": True, "str": "Denver",
"tuple": (1, 2), "frozenset": frozenset({1, 2}), "bytes": b"ok", "None": None,
"list": [1, 2], "dict": {"TX": 1}, "set": {1, 2}, "bytearray": bytearray(b"ok"),
}
for name, value in samples.items():
try:
hash(value)
kind = "immutable"
except TypeError:
kind = "MUTABLE"
print(f"{name:10} {kind}")
Output:
int immutable
float immutable
bool immutable
str immutable
tuple immutable
frozenset immutable
bytes immutable
None immutable
list MUTABLE
dict MUTABLE
set MUTABLE
bytearray MUTABLE
| Immutable | Mutable |
|---|---|
int, float, complex, bool | list |
str | dict |
tuple | set |
frozenset | bytearray |
bytes, None | Most objects of your own classes |
One caution: a tuple is immutable, but if it holds a list, that list can still change. The section on the tuple trap below shows exactly what happens.
Assigning a mutable list doesn’t copy it
This is where mutability usually bites first. backup = prices doesn’t make a copy; it gives the same list a second name:
prices = [19.99, 5.49]
backup = prices # not a copy: a second name for the same list
prices.append(12.00)
print("backup:", backup)
print("same list?", backup is prices)
snapshot = prices.copy() # a real copy
prices.append(3.25)
print("snapshot:", snapshot)
# with an int, changing one name never touches the other
a = 100
b = a
a += 1
print("a =", a, "| b =", b)
Output:
backup: [19.99, 5.49, 12.0]
same list? True
snapshot: [19.99, 5.49, 12.0]
a = 101 | b = 100
Because the list is mutable, a change through either name shows up in both. .copy(), list(prices) or prices[:] make a real copy.
Those all make shallow copies, which matters when the list holds other lists. Shallow copy vs deep copy covers that case.
With the int, nothing surprising happens, because you can’t change 100 in place. If you want to test whether two names point at one object, use is; is vs == explains the difference.
Mutable arguments: why a function can change your list
Python passes the object itself to a function, not a copy. So a function can edit a list you pass in, but it can’t change your int, float or string:
def add_tax(amounts):
"""Changes the caller's list in place (8.25% Texas sales tax)."""
for i in range(len(amounts)):
amounts[i] = round(amounts[i] * 1.0825, 2)
def add_fee(total):
"""Rebinds a local name; the caller's float is untouched."""
total += 2.50
return total
order = [100.00, 40.00]
add_tax(order)
print("list after add_tax:", order)
total = 50.00
add_fee(total)
print("float after add_fee:", total)
total = add_fee(total)
print("float after using the return value:", total)
Output:
list after add_tax: [108.25, 43.3]
float after add_fee: 50.0
float after using the return value: 52.5
add_tax edited the caller’s list directly. add_fee only moved its own local name, so the caller’s total stayed 50.0 until we used the return value.
My rule of thumb: if a function changes a list it was given, say so in its name or docstring. Otherwise, return a new list and leave the input alone.
The mutable default argument bug
This one catches nearly everyone once. A default value is created a single time, when the function is defined, so a mutable default is shared by every call:
def add_item(item, cart=[]): # the default list is created ONCE
cart.append(item)
return cart
print(add_item("pen"))
print(add_item("ink")) # the "empty" cart already holds the pen
def add_item_fixed(item, cart=None): # use None, then make a new list
if cart is None:
cart = []
cart.append(item)
return cart
print(add_item_fixed("pen"))
print(add_item_fixed("ink"))
Output:
['pen']
['pen', 'ink']
['pen']
['ink']
None version starts fresh each time.Use None as the default and create the list inside the function. The same applies to dicts and sets, as default function arguments shows in more detail.
Can a tuple change if it contains a list?
Yes, the list inside it can. The tuple only fixes which objects it holds, not what happens inside them. And there’s a strange case with +=:
order = (["pen"], 4.99) # an immutable tuple holding a mutable list
order[0].append("ink") # changing the list inside is allowed
print(order)
try:
order[0] += ["tape"] # this raises an error...
except TypeError as err:
print("TypeError:", err)
print(order) # ...and still changes the list
Output:
(['pen', 'ink'], 4.99)
TypeError: 'tuple' object does not support item assignment
(['pen', 'ink', 'tape'], 4.99)
order[0] += ["tape"] runs in two steps. First the list extends itself, which works, then Python tries to store the result back into the tuple, which fails.
So you get an error and a changed list. If that surprises you, you’re in good company; it’s one of the best-known oddities in the language.
Why dictionary keys and set items must be immutable
A dictionary finds a key by its hash. If the key could change after you stored it, its hash would change and the dictionary couldn’t find it again, so Python insists on hashable keys:
# tuples are immutable, so they work as dictionary keys
cities = {(30.27, -97.74): "Austin", (39.74, -104.99): "Denver"}
print(cities[(39.74, -104.99)])
# lists are mutable, so they don't
try:
bad = {[30.27, -97.74]: "Austin"}
except TypeError as err:
print("TypeError:", err)
# a set of sets needs frozenset
trips = {frozenset({"TX", "CO"}), frozenset({"NY"})}
print(frozenset({"CO", "TX"}) in trips)
Output:
Denver
TypeError: unhashable type: 'list'
True
Tuples make good keys for coordinates or pairs. When you need a set of sets, use frozenset, the immutable version of a set; set vs tuple compares the two.
Strings are immutable: you can’t change a character
Trying to edit one character in place raises an error. You build a new string instead, and string methods always return a new one:
city = "denver"
try:
city[0] = "D"
except TypeError as err:
print("TypeError:", err)
fixed = "D" + city[1:] # build a new string instead
print(fixed, "| original:", city)
print(city.upper(), "| original:", city) # methods return new strings too
Output:
TypeError: 'str' object does not support item assignment
Denver | original: denver
DENVER | original: denver
That’s why city.upper() didn’t change city. Assign the result if you want to keep it: city = city.upper().
Mutable vs immutable in Python: a quick comparison
| Mutable (list, dict, set) | Immutable (str, int, tuple) | |
|---|---|---|
| Change in place? | Yes | No, you get a new object |
| Same id after a change? | Yes | No |
| Dictionary key or set item? | No | Yes, if everything inside is immutable |
| Safe as a default argument? | No, use None | Yes |
| Can a function change the caller’s value? | Yes | No |
| Copy before editing? | Often | Not needed |
You might want to read these next:
- Shallow copy vs deep copy in Python
- Python set vs tuple
- Create a tuple in Python
- Default function arguments in Python
- The difference between is and == in Python
Frequently asked questions
What is the difference between mutable and immutable in Python?
A mutable object can change after it’s created, like a list or dictionary. An immutable object can’t; operations that look like changes, such as s + "!", create a new object instead.
Is a string mutable in Python?
No. Strings are immutable, so s[0] = "x" raises a TypeError and methods like upper() return a new string.
Is a tuple mutable or immutable?
A tuple is immutable: you can’t add, remove or replace its items. If it holds a mutable object such as a list, that object can still change.
Why can’t a list be a dictionary key?
Dictionary keys must be hashable, and a list isn’t because it can change. Use a tuple instead.
Are integers immutable in Python?
Yes. x += 1 creates a new int and points x at it; the original number never changes.
How do I avoid the mutable default argument problem?
Use None as the default and create the list or dict inside the function: if cart is None: cart = [].
Where is mutability defined officially?
The Python data model describes objects, values and types, including which ones are mutable.
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn