A variable is a label (reference) pointing to an object in memory
β Variables do NOT store values inside them β Variables REFERENCE objects that store values
Variable (Stack) Object (Heap)
x ββββββββββββββΊ [10]
- int β object
- str β object
- list β object
- function β object
- module β object
Variables are just labels pointing to heap objects
a = [1, 2]
b = aa ββ
ββββΊ [1, 2] (one object in heap)
b ββ
Both point to SAME object
x = 10
y = xx ββ
ββββΊ [10] (immutable, typically same object due to caching)
y ββ
Reference copied, not value
list, dict, set
β Modifications change the SAME object
x = [1, 2]
y = x
y.append(3)
print(x) # [1, 2, 3] β x also changed!Before: x ββ
ββββΊ [1, 2]
y ββ
After: x ββ
ββββΊ [1, 2, 3] β SAME object modified
y ββ
int, float, str, tuple
β Any modification creates NEW object
x = 10
y = x
y = y + 1 # or y += 1
print(x) # 10 β x unchanged
print(y) # 11Before: x ββββββΊ [10]
y ββββββ
After: x ββββββΊ [10] (original, unchanged)
y ββββββΊ [11] (new object created)
Changes the object itself
x = [1, 2]
x.append(3) # β mutationx ββββββΊ [1, 2, 3]
(same object, modified)
Points to a different object
x = [1, 2]
x = [3, 4] # β rebindingBefore: x ββββββΊ [1, 2]
After: x ββββββΊ [3, 4] (new object)
| Action | Mutable | Immutable |
|---|---|---|
| Mutation | Same object | N/A |
| Rebinding | New reference | New object |
Function parameter receives a REFERENCE to the same object
def modify(lst):
lst.append(10) # mutation
a = [1, 2]
modify(a)
print(a) # [1, 2, 10] β changed!Inside function:
lst ββ
ββββΊ [1, 2, 10] β SAME object modified
a ββ
def modify(lst):
lst = [999] # rebinding
a = [1, 2]
modify(a)
print(a) # [1, 2] β unchanged!Inside function:
lst ββββββΊ [999] (new local object, function scope only)
a ββββββΊ [1, 2] (unchanged, still original)
a is b # β Are they the SAME object? (identity)
a == b # β Do they have the SAME value? (equality)- Checks if both variables point to same object in memory
- Compares memory addresses
a = [1, 2]
b = a
print(a is b) # True (same object)- Checks if both objects have same value
- Compares content
a = [1, 2]
c = [1, 2]
print(a == c) # True (same value)
print(a is c) # False (different objects)a = [1, 2]
b = a
c = [1, 2]| Expression | Result | Why |
|---|---|---|
a is b |
True |
Same object in memory |
a == b |
True |
Same content |
a is c |
False |
Different objects |
a == c |
True |
Same content |
b = a- Creates new variable
- Points to SAME object
- Both see mutations
a ββ
ββββΊ [1, 2]
b ββ
b = a[:] # or b = a.copy()- Creates NEW object
- Copies content at top level
- Mutations don't affect original
a ββββββΊ [1, 2]
b ββββββΊ [1, 2] (different object, same content)
When analyzing code:
- Track which variable points where
- Not "what value does x hold" but "what object does x refer to"
- Mutable? (list, dict, set)
- Immutable? (int, str, tuple)
- Mutation:
x.append(),x[0] = val,x.pop() - Rebinding:
x = new_value
- Mutable + mutation β shared object changes
- Immutable + rebinding β new object created
- Function argument β reference passed, mutation affects original
x = 10 # β "x stores 10"βοΈ Correct: "x is a reference to object 10"
b = a # β "copied a to b"βοΈ Correct: "b now references the same object as a"
βοΈ Correct: "Functions use call-by-sharing (reference passed)"
x = [1]
x += [2] # βοΈ Mutates (in-place for lists)
y = 10
y += 5 # β Does NOT mutate (creates new int)βοΈ Correct: "Immutable objects create new object, mutable objects may mutate in-place"
- Everything is an object β stored in heap
- Variables are references β labels pointing to objects
- Assignment = reference copy β both point to same object
- Mutable objects β mutation changes shared object
- Immutable objects β reassignment creates new object
1οΈβ£ Draw boxes for objects in heap
2οΈβ£ Draw arrows for references (variables)
3οΈβ£ Identify mutable vs immutable
4οΈβ£ Track mutation vs rebinding
5οΈβ£ Follow function flow
6οΈβ£ Predict output
By end of Level 1, you should:
β
Instantly recognize reference behavior
β
Explain mutation vs reassignment clearly
β
Predict output without running code
β
Distinguish is from ==
β
Handle function arguments confidently
Ready to test? Go to assignement/test folder!
Next Level: Level 2 = nested structures, default arguments, integer caching, string interning, deep copy
Names bind to objects. Assignment rebinds names; mutation changes objects. Keep these two operations separate in your mind.
- Draw name -> object bindings after each line.
- Mark object as mutable or immutable.
- Track aliasing when multiple names point to one object.
- For functions, treat arguments as new local names bound to passed objects.
Scope and binding time are key: LEGB lookup, closure capture, and default-argument evaluation can preserve unexpected state.
- Resolve each variable by LEGB.
- For closures in loops, test late binding assumptions.
- Check whether defaults are evaluated once at function definition.
- Fix with explicit rebinding or immutable defaults when needed.
- After
a=[1,2]; b=a; b.append(3), why doesachange? - In a function, what differs between
x.append(1)andx=[1]? - Why can two variables have equal values but different identities?
- How does late binding in closures create surprising loop behavior?
- Why are mutable default arguments a state-retention trap?
- Which LEGB step do you check first when debugging shadowed variables?
Understand how this topic runs in actual program flow:
- Read statement
- Resolve type/object/reference
- Execute operation (assignment, mutation, call, return)
- Update memory state (stack/heap bindings)
- Re-check final output from updated state
When you see ANY question:
- Primitive? β value
- Object? β reference
- Primitive β stack
- Object β heap (via reference)
- Primitive β copy value
- Object β copy reference
- Field change β mutation
newβ new object (reassignment)
- Always pass-by-value
- Object β reference copied