Mutable vs immutable types
Overview
An object is mutable if its contents can change after it is created, and immutable if they cannot. Lists, dicts, and sets are mutable: list.append() edits the same object in place. int, float, bool, str, and tuple are immutable: 'changing' a string actually builds a brand-new string and leaves the original untouched. Think of a mutable object as a whiteboard you keep editing, and an immutable object as a printed page — to 'change' it you must print a new page. Because mutable objects are edited in place, a string or tuple (as long as it contains only immutable elements) can safely be a dict key while a list cannot.
Mutability decides whether an operation edits an object in place or returns a new one. Getting this wrong causes two classic bugs: assuming a string method changed the string (it didn't), and two names sharing one mutable object so an edit through one is unexpectedly seen through the other.
Where used: Pydantic models, config dictionaries, API request bodies
Why learn this
- Avoiding shared-state bugs when the same list or dict is referenced from two places in an API handler
- Knowing why a tuple or string can be a dict key but a list cannot
Code walkthrough
nums = [1, 2]
nums.append(3)
print(nums)
text = 'hi'
text.upper()
print(text)
Focus: The list prints [1, 2, 3] (changed in place); the string still prints 'hi' because .upper() built a new string we ignored.
Aha moment
cart = ['laptop']
backup = cart
backup.append('mouse')
print(cart)
Prediction: backup looks like a safe copy of cart. After appending 'mouse' to backup, what does print(cart) show?
Common guess: ['laptop']
backup = cart binds a SECOND label to the same list object — it does not copy. So backup.append('mouse') mutates the one list both names point to, and cart sees it too. For an independent copy use cart.copy().
Common mistakes
- Expecting a string to change in place: s.upper() returns a NEW string and leaves s unchanged; calling it without reassigning does nothing. Fix: reassign, e.g. s = s.upper().
- Sharing one mutable object by accident: After b = a where a is a list/dict, a and b are the SAME object, so a.append(...) is visible through b. Fix: copy it (a.copy()) when you need an independent object.
Glossary
- in place
- Modifying the original object directly, rather than creating a brand-new copy of it. Example: `nums.append(4)`.
- shared-state
- When two or more parts of a program access and modify the exact same data in memory, often leading to unexpected bugs. Example: `b.append(1)`.
Recall questions
- What distinguishes a mutable object from an immutable one?
- When you 'change' a string, what actually happens?
- Why can a string or tuple be used as a dict key but a list cannot?
Understanding checks
Why does `print(y)` output `[1, 2, 3, 4]` instead of `[1, 2, 3]`?
Because `y = x` binds the variable `y` to the same underlying list object as `x`. The `+=` operator on a list mutates the object in-place, so `y` reflects the change.
Lists are mutable objects. When multiple variables reference the same mutable object, any in-place mutation to that object through one variable will be visible through all other variables referencing it.
What is the output of this code?
hello
Strings are immutable. `a += ' world'` does not modify the original string 'hello'. Instead, it creates a new string object 'hello world' and reassigns the variable `a` to point to it. The variable `b` still references the original 'hello' string.
Practice tasks
Reassign string to see uppercase
The function below attempts to return an uppercase version of the input string, but it prints the original lowercase text instead. Modify the code to correctly return the uppercase version.
Challenge
Merge config without mutating
Write merge_config(base, override) that returns a NEW dict combining both (override wins) WITHOUT mutating base. Then print the result and base to prove base is unchanged.
Continue learning
Next: Shallow vs deep copy