Assignment (b = a) does not copy the object. It makes another name refer to the same object. Mutations through either name are visible through both.
`copy()` (or list(...), slicing a[:], copy.copy) creates a new outer container. For shallow copies, nested mutables are still shared.
import copy
a = [1, 2, [3, 4]]
# Assignment: same object
b = a
b.append(5)
print(a) # [1, 2, [3, 4], 5]
# Shallow copy: new outer list
c = a.copy()
c.append(6)
print(a) # no trailing 6
c[2].append(99)
print(a) # nested list still shared → [..., [3, 4, 99], 5]
# Deep copy: nested objects copied too
d = copy.deepcopy(a)
d[2].append(100)
print(a[2]) # [3, 4, 99] unchanged by d
print(d[2]) # [3, 4, 99, 100]
# Dicts
cfg = {"retries": 3, "tags": ["etl"]}
cfg2 = cfg # alias
cfg3 = cfg.copy() # shallow copyQuick check in an interview: After b = a, a is b is True. After b = a.copy(), a is b is False, but nested objects may still satisfy a[i] is b[i].