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.
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.18 | list: [4500, 12800] |
str: "Noor Foods" | dict: {"id": "A101"} |
bool: True, False | set: {"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:
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:
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:
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.
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:
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:
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:
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:
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:
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.
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:
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:
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:
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:
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.