CodeOath
← All posts
Python70 min total · 18 parts

Python Fundamentals for Interviews: Data Structures, Comprehensions, and Gotchas

Contents — Part 9 of 18: Mutability, Identity, and is vs. ==
Part 9 of 18 · ~2 min

Mutability, Identity, and is vs. ==

== compares values; is compares identity (are these the exact same object in memory). Mixing them up produces bugs that work by accident for small integers and strings and then break:

a = 1000
b = 1000
a == b   # True — same value
a is b   # False — different objects (usually — small ints -5 to 256 are cached and may print True)

a = "hello"
b = "hello"
a == b   # True
a is b   # often True too, due to string interning — but never rely on this

The rule that actually matters: use == for value comparison, always — reserve is specifically for is None (and is True/is False when you genuinely need to distinguish a boolean from a truthy value) checks, where identity is exactly what you want and CPython guarantees there is only ever one None object.

Mutable vs. immutable, and why it matters for function arguments

Python passes arguments by "assignment" (sometimes called "pass by object reference") — the parameter name inside a function becomes another name pointing at the same object, not a copy of it. Whether that matters depends entirely on whether the object is mutable:

def try_to_modify(lst):
    lst.append(4)          # mutates the SAME list object the caller passed in

def try_to_reassign(lst):
    lst = [99, 99, 99]     # rebinds the LOCAL name "lst" — caller's list is untouched

numbers = [1, 2, 3]
try_to_modify(numbers)
numbers  # [1, 2, 3, 4] — the caller's list WAS changed

try_to_reassign(numbers)
numbers  # [1, 2, 3, 4] — unchanged; reassignment inside the function doesn't propagate out

Mutating an object in place (.append, .sort, dict[key] = x) is visible to every reference to that object, everywhere; rebinding a name only changes what that one local name points to.

Shallow copy vs. deep copy

import copy

original = [[1, 2], [3, 4]]
shallow = original.copy()        # or list(original), or original[:]
deep = copy.deepcopy(original)

shallow[0].append(99)   # mutates original[0] too — shallow copy shares nested objects
original                # [[1, 2, 99], [3, 4]]

deep[1].append(100)      # does NOT affect original — every level was recursively copied
original                # [[1, 2, 99], [3, 4]] — unchanged

A shallow copy duplicates the outer container but keeps references to the same inner objects; a deep copy recursively duplicates everything. Reaching for .copy() on a list of lists (or dicts of lists) and being surprised that mutating a nested item still affects the "copy" is a direct consequence of this.