Mutable Data

How $\mathtt{assignment}$ and shared objects change the rules of reasoning

From trees (structure) to state (change over time)

  • $L06$ takeaway: trees as nested lists $+$ recursive processing
  • New power: programs with local state (history matters)
  • Driving question: how can a function "remember" and update?

Mutation: power and danger

  • Assignment: changes what a name refers to (name $\to$ value)
  • Mutation: changes an existing object's contents "in place"
  • Example: x $=$ [1]; y $=$ x; y.append(2) changes x too

Local state with closures and nonlocal

def make_withdraw(balance):
    """Return a withdraw function with private, mutable balance."""
    def withdraw(amount):
        nonlocal balance
        if amount <= balance:
            balance -= amount
            return balance
        return "Insufficient funds"

    return withdraw

if __name__ == "__main__":
    wd = make_withdraw(100)
    print(wd(25))
    print(wd(25))
    print(wd(60))
    print(wd(15))
  • nonlocal balance = rebind the name in the enclosing frame
  • Same call wd(25) can yield different results over time

Environment view: where does the "memory" live?

  • Frame for make_withdraw: binds balance
  • withdraw closes over that frame
  • Mutation updates the binding balance: 100 → 75 → 50 → 50 → 35 (the 60 withdrawal fails, so no change)

A bank account object (message passing)

def make_account(balance):
    """Return a dispatch function for a bank account with private balance."""
    def withdraw(amount):
        nonlocal balance
        if amount <= balance:
            balance -= amount
            return balance
        return "Insufficient funds"

    def deposit(amount):
        nonlocal balance
        balance += amount
        return balance

    def dispatch(message):
        if message == "withdraw":
            return withdraw
        if message == "deposit":
            return deposit
        raise ValueError(f"Unknown request: {message}")

    return dispatch

if __name__ == "__main__":
    acc = make_account(100)
    print(acc("deposit")(50))
    print(acc("withdraw")(30))
  • Interface: acc("deposit")(50) and acc("withdraw")(30)
  • Abstraction barrier: callers never touch balance directly

Lists: mutation changes the object, not the name

def list_mutation_demo():
    lst = [3, 1, 2]
    lst.append(4)      # in-place
    lst.extend([9, 8]) # in-place
    last = lst.pop()   # removes and returns
    lst.sort()         # in-place sort
    return lst, last

if __name__ == "__main__":
    print(list_mutation_demo())
  • Mutators: append, extend, pop, sort
  • Prediction: What will last be, and what will lst be at the end?
  • Contrast: tuples are immutable (no in-place update)

Aliasing: two names, one object

def aliasing_demo():
    a = [1, 2, 3]
    b = a           # aliasing: b and a refer to the same list
    b.append(4)
    return a, b
def nested_aliasing_demo():
    a = [1, 2]
    b = [a, a]      # the same list appears twice
    a.append(3)
    return b
if __name__ == "__main__":
    print(aliasing_demo())
    print(nested_aliasing_demo())
  • Aliasing test: a change via one name visible via the other
  • Sharing inside structures: [a, a] duplicates a reference, not a copy

Identity vs. equality, and the mutable-default trap

def identity_vs_equality():
    x = [1, 2]
    y = [1, 2]
    a = x
    return {
        "x == y": (x == y),
        "x is y": (x is y),
        "x is a": (x is a),
    }
def append_bad(item, bucket=[]):
    bucket.append(item)
    return bucket
def append_good(item, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket
if __name__ == "__main__":
    print(identity_vs_equality())
    print(append_bad(1))
    print(append_bad(2))
    print(append_good(1))
    print(append_good(2))
  • Predict the printed dictionary: which entries are True vs False?
  • == compares contents; is compares identity (same object)
  • Mutable default arguments persist across calls; use None and create a new list inside

Exit ticket: predict, then explain your model

  • Practice: a=[0]; b=[a,a]; b[0].append(1); print(b)
  • Reflection: when would you choose immutability to avoid aliasing bugs?

Thank you for watching!

Closing illustration

Like, Share, and Subscribe to Thinking in Math

Powered by SciMigo AI Tutor - https://scimigo.com