BugForge
All lessons
Python · 02/10 6 min

Names, mutability and copies

Variables are labels: aliasing, shallow vs deep copies, is vs ==, and hashability.

Names point at objects

a = [1, 2]
b = a            # same list, two names
b.append(3)
a                # [1, 2, 3]

Assignment never copies. Passing a list into a function passes the same list: if the function mutates it, the caller sees the change.

def add_tag(tags, t):
    tags.append(t)       # mutates the caller's list
    return tags

def add_tag(tags, t):
    return [*tags, t]    # returns a new list

Shallow vs deep copy

import copy
orig = {"user": {"name": "a"}, "tags": ["x"]}
shallow = dict(orig)            # or orig.copy(), {**orig}
shallow["tags"].append("y")     # orig["tags"] changed too: inner objects are shared
deep = copy.deepcopy(orig)      # fully independent

list(x), x[:], x.copy() and {**d} are all shallow.

+= on lists mutates

a = [1]; b = a
a += [2]         # in place: b is [1, 2] too
a = a + [3]      # new list: b unaffected

is vs ==

  • == compares values. is compares identity.
  • Use is only for None (and other singletons): if x is None.
  • a is b for small ints or short strings sometimes "works" because of caching. Don't rely on it.

Mutable vs immutable

Immutable Mutable
int, float, str, tuple, frozenset, bytes list, dict, set, most objects

Only hashable (immutable) values can be dict keys or set members. A tuple containing a list isn't hashable. Use tuple(sorted(items)) or frozenset(items) as a key.

Class attributes are shared

class Account:
    history = []                 # ❌ ONE list for every account
    def __init__(self):
        self.history = []        # ✅ per instance
Practise it now
Next in Python
Iterators, generators and comprehensions