Skip to content

Week 2 · Python for real work and SQL

Mutable and immutable objects

Which Python values can change and which can't, and the bugs that come from sharing values that can.

Live session

Some Python values can be changed after they're created, and some can't. Knowing which is which explains a whole family of bugs: data that changes when you didn't touch it, functions that quietly modify their input, and lists that keep growing between calls.

How to follow along

Work in your learn-python project. Create a file called mutability.py, type each example into it and run it with:

uv run mutability.py

Compare what you see with the Output under each example. Clear the file before you type the next example: some examples end in an error on purpose, and an error stops the script there. For those, the output shows only the last line of the traceback, which names the error.

Mutable and immutable

  • Mutable values can be changed in place after they're created. Lists and dictionaries are mutable: you can add, remove and replace what's inside them.
  • Immutable values can never be changed. To get a different value, Python creates a new one. Numbers, strings and booleans are immutable.
Immutable (can't change)Mutable (can change)
int, float: 12800, 0.18list: [4500, 12800]
str: "Noor Foods"dict: {"id": "A101"}
bool: True, Falseset: {"rice", "oil"}
None
tuple: (24.86, 67.01)

You'll meet the tuple and set types later on this page and in later weeks.

Example 1: strings can't change

String methods never change the original string. They return a new one:

mutability.py
customer = "noor foods"
customer.upper()
print(customer)

customer = customer.upper()
print(customer)

Output:

noor foods
NOOR FOODS

The first upper() created "NOOR FOODS" and threw it away, because nothing stored it. To keep the result, assign it back to a variable, as on the second-to-last line. That's why, in week 1, you always wrote email = raw_email.strip().lower().

Trying to change one character of a string fails:

mutability.py
customer = "noor foods"
customer[0] = "N"

Output:

TypeError: 'str' object does not support item assignment

Example 2: lists can change

A list can be changed in place, without creating a new one:

mutability.py
prices = [850, 1200, 400]
prices.append(3100)
prices[0] = 900
print(prices)

Output:

[900, 1200, 400, 3100]

Example 3: watching identity with id()

Every value in Python has an identity, a number you can see with id(). Two values with the same id are the same object in memory.

mutability.py
stock = 120
before = id(stock)
stock += 30
print(id(stock) == before)

prices = [850, 1200]
before = id(prices)
prices.append(400)
print(id(prices) == before)

Output:

False
True

stock += 30 couldn't change the number 120, so Python created a new number, 150, and pointed stock at it. prices.append(400) changed the existing list, so it's still the same object.

Example 4: tuples, the list that can't change

A tuple is like a list that can't be changed. Write it with round brackets. Use one for a group of values that belong together and shouldn't be edited, such as a location's coordinates:

mutability.py
location = (24.86, 67.01)
print(location[0])
location[0] = 31.52

Output:

24.86
TypeError: 'tuple' object does not support item assignment

Example 5: two names, one list

This is the most common mutability bug. Assigning a list to a second variable doesn't copy it. Both names point to the same list:

mutability.py
todays_orders = [4500, 12800]
report_orders = todays_orders
report_orders.append(2300)
print(todays_orders)

Output:

[4500, 12800, 2300]

You only changed report_orders, but todays_orders changed too, because they're the same list. When you need a separate list, make a copy:

mutability.py
todays_orders = [4500, 12800]
report_orders = todays_orders.copy()
report_orders.append(2300)
print(todays_orders)
print(report_orders)

Output:

[4500, 12800]
[4500, 12800, 2300]

Dictionaries have a .copy() method too.

Example 6: functions can change your data

When you pass a list or dictionary to a function, the function receives the same object, not a copy. If the function changes it, your data changes:

mutability.py
def add_delivery_charge(totals):
    totals.append(350)


orders = [4500, 12800]
add_delivery_charge(orders)
print(orders)

Output:

[4500, 12800, 350]

An immutable value can't be changed this way. The function can only create a new value inside itself:

mutability.py
def add_tax(total):
    total = total * 1.18


amount = 1000
add_tax(amount)
print(amount)

Output:

1000

To get the new value out, return it, as you did in week 1. A good rule: a function should either return a new value or change its input, and its name should make clear which.

Example 7: copies of nested data

.copy() makes a shallow copy: a new outer list or dictionary, but anything nested inside is still shared.

mutability.py
order = {"id": "A101", "items": ["rice", "oil"]}
draft = order.copy()
draft["id"] = "A102"
draft["items"].append("sugar")
print(order)

Output:

{'id': 'A101', 'items': ['rice', 'oil', 'sugar']}

Changing draft["id"] didn't affect order, but adding to draft["items"] did, because both dictionaries share the same items list. For a fully independent copy, use deepcopy() from the standard library's copy module. Put the import at the top of your file:

mutability.py
import copy

order = {"id": "A101", "items": ["rice", "oil"]}
draft = copy.deepcopy(order)
draft["items"].append("sugar")
print(order)
print(draft)

Output:

{'id': 'A101', 'items': ['rice', 'oil']}
{'id': 'A101', 'items': ['rice', 'oil', 'sugar']}

Data from JSON files and APIs is almost always nested like this, so reach for deepcopy() when you need to change a copy of it.

Example 8: the default value trap

In week 1 you gave function parameters default values, such as rate=0.18. Never use a mutable value, such as [] or {}, as a default:

mutability.py
def add_item(item, cart=[]):
    cart.append(item)
    return cart


print(add_item("rice"))
print(add_item("oil"))

Output:

['rice']
['rice', 'oil']

The second call should have returned a cart with only oil in it. Python creates the default list once, when the function is defined, and every call that uses the default shares it. The fix is to use None as the default and create a new list inside the function:

mutability.py
def add_item(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart


print(add_item("rice"))
print(add_item("oil"))

Output:

['rice']
['oil']

Example 9: dictionary keys must be immutable

Dictionary keys can't change, so they must be immutable. A tuple works as a key; a list doesn't:

mutability.py
stock_by_area = {("Karachi", "Clifton"): 12}
print(stock_by_area[("Karachi", "Clifton")])

bad = {["Karachi", "Clifton"]: 12}

Output:

12
TypeError: cannot use 'list' as a dict key (unhashable type: 'list')

Why this matters for an FDE

Real systems pass the same data through many functions: one loads it, another cleans it, another builds a report. If one of them changes a shared list or dictionary by accident, a value somewhere else changes with no error and no warning. These bugs are hard to find because the line that breaks isn't the line that caused it. Knowing what can change, and copying when you need to, prevents them.