Mutable vs immutable types

RoadmapsPython Backend

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

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

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

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

Next: Mutable default argument pitfall

Return to Python Backend Roadmap