015. Mutable Vs Immutable Types
Mutable state enables aliasing bugs
015. Mutable Vs Immutable Types
Aryan’s RAM monitor has a whitelist of processes that should never be killed. The default whitelist is ["init", "launchd", "kernel_task"].
He writes a function that gives each monitoring job its own whitelist — the idea being that each job can add custom processes to protect.
DEFAULT_WHITELIST = ["init", "launchd", "kernel_task"]
def get_job_whitelist(job_id):
return DEFAULT_WHITELISTJob A adds “postgres” to its whitelist. Job B starts up and the monitor protects “postgres” — even though Job B never asked for it.
The function returns the SAME list object to every caller. When Job A mutates it, all other jobs see the mutation. This is the aliasing bug.
Fix the tags function below using the same lesson: return a .copy().
💡 Fun fact: Python’s immutable types (int, str, tuple) are safe to share freely — no copy needed. The designers made strings immutable specifically so they can be used as dictionary keys without worrying about a key changing after insertion. If strings were mutable, every dict lookup would require a defensive copy.
⚠️ Watch out: The most common mutable default argument trap is def f(items=[]) — the list is created once when the function is defined, not each time it is called. Every call that appends to items modifies the same shared list. The Python idiom is def f(items=None) then if items is None: items = [].
🤔 Think about it: .copy() creates a shallow copy — nested lists or dicts inside the list are still shared. When would a shallow copy be insufficient, and what would you use instead?
Learning objectives
- Distinguish mutable (list, dict) from immutable (str, tuple) types
- Understand that returning a mutable object shares it, not copies it
- Use .copy() to create independent copies of mutable objects
Key concepts
- mutable
- immutable
- aliasing
- copy
Try it
Concept detail
Mutable objects (list, dict, set) can be changed in place after creation. Immutable objects (int, float, str, tuple, frozenset) cannot be changed — operations always produce new objects.
The aliasing trap: a = [1, 2, 3] b = a # b is NOT a copy — it’s the same object b.append(4) print(a) # [1, 2, 3, 4] — a was mutated through b!
This is the bug in broken_code: get_user_tags() returns DEFAULT_TAGS (the same object). alice_tags and bob_tags are two names for ONE list. Appending to alice_tags also changes bob_tags — and DEFAULT_TAGS itself.
The fix: return DEFAULT_TAGS.copy(). .copy() creates a shallow copy — a new list object with the same element references. For nested structures (lists of lists, dicts of lists), use copy.deepcopy().
Why immutability is safer: if you return a tuple or string, the caller cannot mutate the original. Immutability is a design tool: prefer tuples for data that should not change, and copy mutable defaults before handing them out.
Solution
DEFAULT_TAGS = ["news", "sports"]
def get_user_tags(user_id):
return DEFAULT_TAGS.copy()
# Simulate two users
alice_tags = get_user_tags("alice")
alice_tags.append("music")
bob_tags = get_user_tags("bob")Tests
def test_bob_unaffected_by_alice():
assert bob_tags == ["news", "sports"]
def test_alice_has_music():
assert "music" in alice_tags
def test_alice_and_bob_are_different_objects():
assert alice_tags is not bob_tags
def test_default_unchanged():
assert DEFAULT_TAGS == ["news", "sports"]
def test_bob_has_default_only():
assert len(bob_tags) == 2