Last week I was helping a friend debug a Python script. She had written something like this:
cart = ["apple", "banana"]
saved_cart = cart
cart.append("mango")
print(saved_cart)
She expected saved_cart to stay ["apple", "banana"].
It didn't.
It printed ["apple", "banana", "mango"].
She stared at her screen for a full minute. "I never touched saved_cart. How did it change?"
That question is the reason this post exists.
Because the answer is not just a Python thing. The same trap exists in JavaScript, but the fix is different in each language. And if you don't understand what is happening in memory, you will keep falling into this hole every few months.
I deleted six variables across two languages to show you exactly what is going on.
Let's look inside.
The mental model most beginners start with (and why it breaks)
When you first learn variables, every tutorial says something like:
"A variable is a box that stores a value."
That model works for maybe a week. Then you try to copy a list, and the model falls apart.
Here is the problem: Python doesn't put values in boxes. Python sticks name tags on objects.
Think of it this way:
When you write a = [1, 2, 3], Python does two things:
- Creates a list object
[1, 2, 3]somewhere in memory - Sticks the name tag
aon that object
When you then write b = a, Python does not copy the list. It sticks a second name tag on the same object.
Now both a and b point to the same list. Change the list through either name, and the other name sees it too.
Let me prove it:
a = [1, 2, 3]
b = a
print(id(a)) # 140234866534400
print(id(b)) # 140234866534400 <-- same memory address!
print(a is b) # True -- they are literally the same object
The id() function returns the memory address of an object. Same address means same object. Not a copy. The actual, same, single object.
JavaScript does the same thing (but nobody warns you)
Here is the thing that trips up developers who switch between Python and JavaScript: the behavior is identical for arrays and objects.
let a = [1, 2, 3];
let b = a;
b.push(4);
console.log(a); // [1, 2, 3, 4] -- wait, what?
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4] -- same surprise
Both languages store arrays (lists) as references. When you assign one variable to another, you are copying the reference, not the data.
But here is where the two languages start to differ.
Where Python and JavaScript actually split
The difference shows up with simple values like numbers and strings.
JavaScript has two kinds of values:
- Primitives (number, string, boolean, null, undefined, symbol, bigint) are copied by value
- Objects (array, object, function) are copied by reference
Python doesn't have that split. In Python, everything is an object. But some objects are mutable (you can change them) and some are immutable (you cannot change them).
Here is the practical difference:
# Immutable types: int, str, tuple, frozenset, bool
x = 5
y = x
y = 10
print(x) # 5 -- x is unchanged, because integers are immutable
# y = 10 created a NEW integer object
# and pointed y at the new one
# Mutable types: list, dict, set, custom objects
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4] -- a changed because lists are mutable
# b.append() modified the SAME object
# that both a and b point to
In JavaScript:
// Primitives: copied by value
let x = 5;
let y = x;
y = 10;
console.log(x); // 5 -- no surprise
// Objects/Arrays: copied by reference
let a = [1, 2, 3];
let b = a;
b.push(4);
console.log(a); // [1, 2, 3, 4] -- same trap
The rule to remember:
Python: Immutable objects are safe. Mutable objects are shared.
JavaScript: Primitives are safe. Objects and arrays are shared.
The 3 traps that catch every beginner
Now that you understand the model, let me show you the three places where this catches people in real code.
Trap 1: "I copied the list but it still changed"
This is the most common one:
original = [1, 2, [3, 4]]
backup = original.copy() # shallow copy!
backup[2].append(5)
print(original) # [1, 2, [3, 4, 5]] -- your "backup" broke the original
Wait. You called .copy(). How did the original change?
Because .copy() makes a shallow copy. It copies the outer list, but the inner list [3, 4] is still shared.
The fix:
import copy
original = [1, 2, [3, 4]]
backup = copy.deepcopy(original) # deep copy -- truly independent
backup[2].append(5)
print(original) # [1, 2, [3, 4]] -- safe!
In JavaScript, the same concept applies:
let original = [1, 2, [3, 4]];
let backup = [...original]; // shallow copy using spread
backup[2].push(5);
console.log(original); // [1, 2, [3, 4, 5]] -- same trap!
// The fix:
let safeCopy = structuredClone(original); // deep copy (modern JS)
safeCopy[2].push(5);
console.log(original); // [1, 2, [3, 4]] -- safe!
Quick reference for copying:
| What you want | Python | JavaScript |
|---|---|---|
| Shallow copy (list) |
b = a.copy() or b = list(a)
|
b = [...a] or b = Array.from(a)
|
| Shallow copy (dict/obj) |
b = a.copy() or b = dict(a)
|
b = {...a} or Object.assign({}, a)
|
| Deep copy | copy.deepcopy(a) |
structuredClone(a) |
Trap 2: "My function remembers things it shouldn't"
This one is Python-specific, and it is weird.
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item("apple")) # ["apple"]
print(add_item("banana")) # ["apple", "banana"] -- ???
print(add_item("cherry")) # ["apple", "banana", "cherry"] -- WHAT?
Each call is supposed to start with an empty basket. But it doesn't.
Here is why: Python evaluates default arguments once, when the function is defined. Not every time the function is called. That empty list [] is created once and reused on every call.
The fix:
def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
print(add_item("apple")) # ["apple"]
print(add_item("banana")) # ["banana"] -- correct!
print(add_item("cherry")) # ["cherry"] -- correct!
Use None as the default and create the list inside the function body. This is such a common pattern that experienced Python developers do it automatically without thinking.
JavaScript doesn't have this problem because default parameters are evaluated fresh on every call:
function addItem(item, basket = []) {
basket.push(item);
return basket;
}
console.log(addItem("apple")); // ["apple"]
console.log(addItem("banana")); // ["banana"] -- fresh basket each time
Score one for JavaScript on this round.
Trap 3: "I changed the value inside the function but it didn't work"
def double_it(x):
x = x * 2
score = 10
double_it(score)
print(score) # 10 -- not 20!
The function didn't change score because integers are immutable. Inside the function, x = x * 2 created a new integer object 20 and pointed the local name x at it. The original score still points to 10.
But with mutable objects:
def add_bonus(scores):
scores.append(100)
my_scores = [85, 90, 95]
add_bonus(my_scores)
print(my_scores) # [85, 90, 95, 100] -- changed!
This works because scores.append(100) modifies the existing list object that my_scores also points to.
The rule:
Reassignment inside a function does not affect the outside.
Mutation inside a function does affect the outside.
This is the same in both Python and JavaScript:
function doubleIt(x) {
x = x * 2; // reassignment -- no outside effect
}
let score = 10;
doubleIt(score);
console.log(score); // 10
function addBonus(scores) {
scores.push(100); // mutation -- affects outside
}
let myScores = [85, 90, 95];
addBonus(myScores);
console.log(myScores); // [85, 90, 95, 100]
The cheat sheet you can screenshot
Here is everything in one place:
PYTHON JAVASCRIPT
------ ----------
Everything is an object Primitives vs Objects
Immutable = safe to share Primitives = safe to share
Mutable = watch out Objects/Arrays = watch out
a = b --> same object let b = a --> same reference
.copy() --> shallow [...a] --> shallow
deepcopy --> full clone structuredClone --> full clone
Default args evaluated ONCE Default params evaluated EACH call
None pattern for safe defaults No workaround needed
id(a) == id(b) means same obj a === b means same reference
One thing I wish I learned earlier
The single best debugging tool for this entire class of bugs is id() in Python and comparing with === in JavaScript.
Before you start wondering why your data changed, just check:
# Python
a = [1, 2, 3]
b = a
print(f"Same object? {a is b}") # True
b = a.copy()
print(f"Same object? {a is b}") # False -- now they're separate
// JavaScript
let a = [1, 2, 3];
let b = a;
console.log(a === b); // true -- same reference
b = [...a];
console.log(a === b); // false -- different objects now
That one check would have saved me hours the first year I was writing Python.
Where to go from here
This post covered the core model. But there is more to dig into if you are curious:
-
Python's
__slots__: How to lock down object attributes and save memory - WeakRef in JavaScript: References that don't prevent garbage collection
-
Python's
gcmodule: Watching the garbage collector work in real time -
Immutable patterns in JavaScript:
Object.freeze(), Immer, and when they matter
If even one of those sounds interesting, you now have the foundation to understand why they exist.
What tripped you up first: Python's mutable defaults, or JavaScript's shared arrays? Or was it something completely different that nobody warned you about? Drop it below. I genuinely want to know.





Top comments (0)