Module 1
The object model & mutability
The single most useful mental model in Python: variables are not boxes. Every value — an int, a list, a function, a class — is an object living on the heap, and a variable is just a name bound to an object, like a sticky note on it. Assignment moves the sticky note; it never copies the object.
a = [1, 2] b = a # no copy — b is a second name for the SAME list b.append(3) print(a) # [1, 2, 3] — "a" changed, because there is only one list
So a = b creates an alias. Whether that alias can hurt you depends on one thing: is the shared object mutable?
Python splits its built-ins cleanly. Mutable: list, dict, set — they can change in place, so every alias sees the change. Immutable: int, str, tuple, frozenset— “changing” them always means building a new object and rebinding the name, so aliases to immutables are harmless.
s = "abc" t = s t += "d" # builds a NEW string "abcd" and rebinds t print(s) # "abc" — untouched. Immutable aliasing is safe.
This is also why is and == are different questions. == asks “do these objects have equal values?”; is asks “are these the same object — same id()?” A classic footgun: CPython interns small integers (−5..256) and many short strings, so is sometimes appearsto work for value comparison — until it doesn't:
x = 256; y = 256 x is y # True — small ints are interned (cached) x = 257; y = 257 x is y # False (typically) — two distinct objects, equal values x == y # True — always the right question for values
Rule: use is only for singletons (is None, is True, sentinels). Use == for values.
🎛 Names & objects lab
Names
Objects (the heap)
[1, 2]
list
A simplified picture of CPython's real model: every value is a heap object with an identity (id()), and a variable is an entry in a namespace dict mapping a name to an object. Assignment rebinds the name; methods like .append() mutate the object itself. Ids shown are illustrative. Educational tool.
The senior-interview classic: mutable default arguments
Default argument values are evaluated once — at def time — and stored on the function object. If the default is mutable, every call that omits the argument shares the same object:
def add_trade(trade, acc=[]): # [] built ONCE, at def time
acc.append(trade)
return acc
add_trade("AAPL") # ['AAPL']
add_trade("MSFT") # ['AAPL', 'MSFT'] <- surprise: same list!
add_trade("GOOG") # ['AAPL', 'MSFT', 'GOOG']The function looks pure but carries hidden state between calls. The idiom every senior engineer writes on autopilot: default to None and build the fresh object inside the call:
def add_trade(trade, acc=None):
if acc is None: # "is None" — the correct use of "is"
acc = [] # new list PER CALL
acc.append(trade)
return accSame trap, different costumes: def f(when=datetime.now()) freezes the timestamp at import time, and def f(cfg={}) shares one dict across all callers.
Aliasing bugs in real systems — and defensive copying
The production version of this bug: you pass a list into a cache or a config object, keep your own reference, and mutate it later. The cache is now silently corrupted — no line of cache code ever ran:
cache = {}
tickers = ["AAPL", "MSFT"]
cache["universe"] = tickers # cache holds an ALIAS, not a snapshot
tickers.append("ENRN") # ...much later, far away
cache["universe"] # ['AAPL', 'MSFT', 'ENRN'] — corruptedThree defenses, in order of preference:
- Copy at the boundary: cache["universe"] = list(tickers) — a shallow copy snapshots the container. Note it does not copy nested objects; for a list of dicts you need copy.deepcopy (recursive, and much more expensive — reach for it deliberately, not by default).
- Store immutables: tuple(tickers) is cheap immutability — nobody can mutate it, so aliasing becomes free and safe.
- Don't hold references you don't own — a rule that matters double for anything long-lived (caches, module-level config, class attributes).
If this instinct feels familiar from the Java side, it should: it's the same “validated immutable value” discipline behind records — see Records & immutability in the Financial Engineering track. In Python the equivalents are tuples, frozenset, and frozen=True dataclasses (Module 3).
Things to try
- • Run the aliasing scenario in the lab and watch both names point at one object — then run rebinding and see why b = [9] can never affect a.
- • Step through the mutable default scenario: the default list is created once and survives between calls.
- • In a real Python shell, compare id(a) before and after a += [3] (same id — in-place) vs a = a + [3] (new id — rebinding).
- • Predict, then check: t = ([1], [2]); t[0].append(9) — an immutable tuple holding mutable lists. Immutability is one level deep.