← Course Home
Module 4

Mutability

Why two variables can secretly be the same list — and why that catches people off guard.

Concept

Mutable vs Immutable

A mutable thing can be changed after it's created, in place — lists and dicts work this way. An immutable thing can't — strings, numbers, booleans, and None (everything from Module 1) never change once made. "Changing" one of those actually makes a brand new one.

colors = ["red"]
colors.append("blue")   # same list, changed in place

x = 5
x = x + 1                # NOT the same 5 changed -- a new 6, x just points to it now
Lab 1

Is a list mutable or immutable?

Mutable
Lab 2

Is a string mutable or immutable?

Immutable
Lab 3

Is a dict mutable or immutable?

Mutable
Lab 4

Is an integer mutable or immutable?

Immutable
Lab 5

colors = ["red"]. Append "blue", print colors — this is mutating, not reassigning.

['red', 'blue']
Now go practice

Open problems/01_mutable_vs_immutable.py and do all 5 problems locally.

Concept

Reassigning vs Mutating

These look similar but are completely different: mutating changes the object itself (.append()). Reassigning just points the variable at a different object (x = x + [3] builds a new list).

a = [1, 2]
a.append(3)      # mutate -- same list, now [1, 2, 3]

b = [1, 2]
b = b + [3]      # reassign -- a brand new list, b just points to it now
Lab 1

a = [1, 2]. Append 3 (mutate), print a.

[1, 2, 3]
Lab 2

b = [1, 2]. Reassign b = b + [3], print b.

[1, 2, 3] — same visible result, different mechanism
Lab 3

x = "hi". Reassign x = x + "!", print x.

hi! — strings can only ever be reassigned, never mutated
Lab 4

d = {"a": 1}. Mutate it by adding a key, print d.

{'a': 1, 'b': 2} (whatever you added)
Lab 5

Is d["a"] = 2 (changing a dict's value) a mutation or a reassignment of d itself?

A mutation — d still points to the same dict
Lab 6

count = 0. Reassign count = count + 1 three times in a row, print the final value.

3
Now go practice

Open problems/02_reassign_vs_mutate.py and do all 6 problems locally.

Concept

Two Names, One List

b = a does not copy the list — it makes b point at the exact same list as a. Change one, and both variables show the change, because there's only one actual list.

a = [1, 2]
b = a          # b points at the same list as a, not a copy
b.append(3)
print(a)       # [1, 2, 3] -- a changed too!
Lab 1

a = ["red"]. b = a. Append "blue" to b. Print a.

['red', 'blue'] — a changed even though you only touched b
Lab 2

Same setup. Print b too — confirm it matches a.

['red', 'blue']
Lab 3

a = [1, 2, 3]. b = a. Pop the last item off b. Print a.

[1, 2]
Lab 4

a = [1]. b = a. Insert 0 at position 0 in a. Print b — the change shows up on the other name too.

[0, 1]
Predict first

a = [1, 2]. b = a. b.append(3). What does print(len(a)) print?

Check
3

a and b are the same list — appending through b grew a too.

Now go practice

Open problems/03_alias_list.py and do all 6 problems locally.

Concept

Two Names, One Dict

Same rule as lists — b = a on a dict points b at the same dict, not a copy.

a = {"status": "ACTIVE"}
b = a
b["status"] = "SUSPENDED"
print(a)   # {'status': 'SUSPENDED'} -- a changed too
Lab 1

a = {"status": "ACTIVE"}. b = a. Update b["status"] to "SUSPENDED". Print a.

{'status': 'SUSPENDED'}
Lab 2

Same setup. Print a is b — confirms they're the exact same dict.

True
Lab 3

a = {"x": 1}. b = a. Add a new key to b. Print a.

has the new key too
Lab 4

a = {"x": 1, "y": 2}. b = a. Pop "y" from b. Print a.

{'x': 1}
Lab 5

a = {}. b = a. Add a key to a this time (not b). Print b — same result either direction.

has the new key
Lab 6

Predict then check: a = {"a": 1}. b = a. c = b. Update c["a"] to 99. Print a — three names, still one dict.

{'a': 99}
Now go practice

Open problems/04_alias_dict.py and do all 6 problems locally.

Concept

Same Value, Different Object

Two separately created lists can hold identical values (== says True) while still being two different objects in memory (is says False). is only says True when it's genuinely the same object — which is exactly what aliasing (b = a) creates.

a = [1, 2]
b = [1, 2]      # a separate list that happens to look the same
print(a == b)    # True -- same values
print(a is b)    # False -- different objects

c = a
print(a is c)    # True -- c is literally a, from aliasing
Lab 1

a = [1, 2], b = [1, 2] (built separately). Print a == b.

True
Lab 2

Same two lists. Print a is b.

False
Lab 3

a = [1, 2]. c = a (aliased). Print a is c.

True
Lab 4

x = {"a": 1}, y = {"a": 1} (built separately). Print x == y, then x is y — two lines.

True then False
Lab 5

Which one — == or is — would tell you whether mutating one list will affect another? Print a demonstration proving your answer.

is
Lab 6

Recall Module 1: x = None. Print x is None — the same is, a different context.

True
Lab 7 · Checkpoint

One script: create a list a, alias it as b, mutate through b, print a to show the effect, then print a is b to explain why.

Combines sub-skills 1–5.
Now go practice

Open problems/05_is_vs_eq.py — 7 problems, #7 is a checkpoint combining sub-skills 1–5.

Concept

Making a Real Copy of a List

.copy() makes an actual second list — a new object with the same values, not just another name for the same one. Now mutating one doesn't affect the other.

a = [1, 2]
b = a.copy()
b.append(3)
print(a)   # [1, 2] -- unaffected this time
Lab 1

a = ["red"]. b = a.copy(). Append "blue" to b. Print a.

['red'] — unaffected
Lab 2

Same setup. Print b — it has the change, a doesn't.

['red', 'blue']
Lab 3

Print a is b after copying — confirms they're different objects now.

False
Lab 4

Print a == b right after copying, before any mutation — still equal in value.

True
Lab 5

Another way to copy: b = a[:] (slicing the whole thing). Try it, mutate b, print a unaffected.

Lab 6

Copy a list, remove an item from the copy, print both — only the copy shrank.

Now go practice

Open problems/06_copy_list.py and do all 6 problems locally.

Concept

Making a Real Copy of a Dict

Same idea — .copy() on a dict makes a real second dict.

a = {"status": "ACTIVE"}
b = a.copy()
b["status"] = "SUSPENDED"
print(a)   # {'status': 'ACTIVE'} -- unaffected
Lab 1

a = {"status": "ACTIVE"}. b = a.copy(). Update b["status"]. Print a.

{'status': 'ACTIVE'} — unaffected
Lab 2

Same setup. Print a is b.

False
Lab 3

Print a == b right after copying, before mutating.

True
Lab 4

Copy a dict, add a new key to the copy only, print both to confirm only the copy changed.

Lab 5

Copy a dict, pop a key from the original, print the copy to confirm it still has that key.

Lab 6

Copy, then check len() of both — same right after copying.

Now go practice

Open problems/07_copy_dict.py and do all 6 problems locally.

Concept

Copies Are Shallow

Here's the trap: .copy() only copies the outer object. If a value inside is itself a list or dict, both copies still point at that same inner object.

a = {"profile": {"email": "test@example.com"}}
b = a.copy()
b["profile"]["email"] = "changed@example.com"
print(a)   # {'profile': {'email': 'changed@example.com'}} -- a changed too!
Predict first

a = {"profile": {"dept": "IT"}}. b = a.copy(). b["profile"]["dept"] = "Sales". What does print(a["profile"]["dept"]) show?

Check
Salesa changed too

The outer dict got copied, but the inner profile dict is the same one both copies point at. .copy() only goes one level deep — this is why it's called a "shallow" copy.

Lab 1

Confirm it yourself: build the example above, print a["profile"] is b["profile"].

True — same inner dict
Lab 2

Print a is b in the same setup — the outer dicts, at least, are different.

False
Lab 3

Same trap with a list inside a dict: a = {"groups": ["IT"]}. Copy it, append to b["groups"], print a["groups"].

changed too
Lab 4

Print a["groups"] is b["groups"] for the setup above.

True
Lab 5

Contrast: a dict with only flat (non-list, non-dict) values — copy it, mutate the copy's flat value, print the original. This one is safe.

original unaffected
Now go practice

Open problems/08_shallow_copy.py and do all 7 problems locally.

Concept

Why Immutables Are Safe

Strings and numbers never have any of these problems, because they can never be mutated — only reassigned. Aliasing one just means both names point at the same unchangeable value, and there's nothing to accidentally break.

a = "hello"
b = a
b = b + " world"
print(a)   # hello -- completely unaffected, no aliasing risk existed
Lab 1

a = "hi". b = a. Reassign b = b + "!". Print a.

hi — unaffected
Lab 2

x = 5. y = x. Reassign y = y + 1. Print x.

5 — unaffected
Lab 3

Print x is y for the setup above, after the reassignment.

False — y now points somewhere new
Lab 4

Proof strings really can't be mutated: s = "hi". This SHOULD crash: s[0] = "H". Write it and run it — watch the error.

CrashesTypeError: 'str' object does not support item assignment
Lab 5

Alias a number 3 times (a = 1, b = a, c = b), reassign only c, print all three.

1\n1\nnew value
Lab 6

Same experiment with a list instead: alias 3 times, mutate (not reassign) through the third name, print all three.

all three show the change
Lab 7

Print "hello" is "hello" — two identical string literals. Python will actually warn you about this line.

True, but with a SyntaxWarning: "is" with 'str' literal. Did you mean "=="?

This is the real lesson: is is for checking None or confirming aliasing (same object), never for comparing values — Python itself tells you to use == instead. That it happens to say True here is an implementation detail, not something to rely on.

Lab 8 · Final checkpoint

One script covering the whole module: alias a list and show the mutation bug, copy a list properly, alias a dict with a nested dict and hit the shallow-copy gotcha, and reassign a string to prove it's unaffected. Print a result after each step.

This is the big one — take your time.
Now go practice

Open problems/09_immutables_safe.py — 8 problems, #8 is the final checkpoint for all of Module 4.

Complete

Module 4 done

mutable vs immutable → reassign vs mutate → aliasing (list, dict) → is vs == → real copies → shallow copy gotcha → why immutables are safe.

Run python check.py in problems/ to grade everything.