DEV Community

Cover image for I Mass-Deleted 6 Variables to Prove Python and JavaScript Handle Memory Differently
S M Tahosin
S M Tahosin Subscriber Community Curator

Posted on

I Mass-Deleted 6 Variables to Prove Python and JavaScript Handle Memory Differently

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)
Enter fullscreen mode Exit fullscreen mode

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:

Python names point to objects, not boxes

When you write a = [1, 2, 3], Python does two things:

  1. Creates a list object [1, 2, 3] somewhere in memory
  2. Sticks the name tag a on 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
Enter fullscreen mode Exit fullscreen mode

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.

JavaScript and Python share the same reference trap for mutable objects

let a = [1, 2, 3];
let b = a;
b.push(4);

console.log(a); // [1, 2, 3, 4]  -- wait, what?
Enter fullscreen mode Exit fullscreen mode
a = [1, 2, 3]
b = a
b.append(4)

print(a)  # [1, 2, 3, 4]  -- same surprise
Enter fullscreen mode Exit fullscreen mode

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).

Mutable vs Immutable types in Python

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
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Shallow copy vs deep copy explained visually

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!
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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.

The mutable default argument trap in Python functions

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?
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// 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
Enter fullscreen mode Exit fullscreen mode

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 gc module: 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)