DEV Community

Cover image for The Array Bug That Looks Different in PHP, Python, and JavaScript (But Is Really the Same Bug)
Bikki Singh
Bikki Singh

Posted on • Originally published at codepractice.in

The Array Bug That Looks Different in PHP, Python, and JavaScript (But Is Really the Same Bug)

💡 TL;DR

PHP copies arrays by default. Python and JavaScript share references by default. If you switch between these languages and don't switch your mental model with them, you'll spend hours debugging an array that "shouldn't" have changed. Here's the breakdown, with code, for all three.


The Bug Nobody Warns You About

You pass an array into a function, mutate it, then check the original — and it's changed too. Or the reverse: you expected a shared reference and got a clean copy instead.

It's not really one bug. It's three different language models wearing the same disguise.

PHP arrays are value types (copied by default). Python lists are reference types (shared by default). JavaScript arrays are objects passed by reference — with a const twist that catches even seniors off guard.

If you work across a PHP backend, Python scripts, and a JS frontend (and let's be honest, a lot of us do), this is the bug that gets you when you least expect it — usually three function calls deep into a real request, long after the original array is gone.

PHP: Copy First, Reference on Request

PHP arrays use copy-on-write semantics. Assign one to a new variable, or pass it into a function, and you get an independent copy by default.

function addDiscount(array $cart): array {
    $cart[] = 'discount_code_applied'; // local copy only
    return $cart;
}

$userCart = ['apple', 'banana'];
addDiscount($userCart);

print_r($userCart);
// ['apple', 'banana'] — unchanged
Enter fullscreen mode Exit fullscreen mode

Want to mutate the original instead? You have to ask for it explicitly with &:

function applyDiscount(array &$cart): void {
    $cart[] = 'discount_code_applied'; // this IS the original
}
Enter fullscreen mode Exit fullscreen mode

No & means copy. & means reference. PHP makes you say what you mean — which honestly isn't a bad design choice once you internalize it.

Python: Reference First, Copy on Request

This is where most people coming from PHP get burned. Assigning a Python list doesn't copy it — it just creates a second name pointing at the same object.

original_cart = ['apple', 'banana']
temp_cart = original_cart
temp_cart.append('cherry')

print(original_cart)
# ['apple', 'banana', 'cherry'] — yep, it changed
print(temp_cart is original_cart)
# True — literally the same object
Enter fullscreen mode Exit fullscreen mode

Same story inside functions:

def add_to_cart(cart):
    cart.append('discount_applied')  # mutates caller's list

user_cart = ['apple', 'banana']
add_to_cart(user_cart)
print(user_cart)
# ['apple', 'banana', 'discount_applied']
Enter fullscreen mode Exit fullscreen mode

To actually get a copy, be explicit:

temp = original_cart[:]        # shallow copy
temp = list(original_cart)     # also shallow
import copy
temp = copy.deepcopy(original_cart)  # deep copy
Enter fullscreen mode Exit fullscreen mode

Watch out for nested lists — a shallow copy still shares inner lists:

original = ['apple', ['mango', 'grape']]
shallow = original[:]
shallow[1].append('kiwi')
print(original)  # ['apple', ['mango', 'grape', 'kiwi']] — leaked!
Enter fullscreen mode Exit fullscreen mode

deepcopy is the only thing that fully isolates nested structures.

The classic Python trap nobody escapes the first time

def add_item(item, cart=[]):  # created ONCE, reused forever
    cart.append(item)
    return cart

print(add_item('apple'))   # ['apple']
print(add_item('banana'))  # ['apple', 'banana'] — wait what
Enter fullscreen mode Exit fullscreen mode

Mutable default arguments are evaluated once, at function definition time. Fix it with None:

def add_item(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart
Enter fullscreen mode Exit fullscreen mode

JavaScript: Reference, Plus a const Curveball

JS arrays are objects, so they behave like Python at first glance — shared by reference.

const originalCart = ['apple', 'banana'];
const tempCart = originalCart;
tempCart.push('cherry');

console.log(originalCart);
// ['apple', 'banana', 'cherry']
Enter fullscreen mode Exit fullscreen mode

Here's the part that confuses people: const does not make the array immutable. It only blocks reassigning the variable.

const userCart = ['apple', 'banana'];
userCart.push('cherry'); // totally fine
// userCart = ['new']; // TypeError — this is what const actually blocks
Enter fullscreen mode Exit fullscreen mode

So a function like this will silently mutate your caller's data:

function applyDiscount(cart) {
    cart.push('discount_applied'); // mutates the original
    return cart;
}
Enter fullscreen mode Exit fullscreen mode

Fix it by copying first:

function applyDiscount(cart) {
    const cartCopy = [...cart]; // shallow copy
    cartCopy.push('discount_applied');
    return cartCopy;
}
Enter fullscreen mode Exit fullscreen mode

For arrays of objects, [...cart] isn't enough — go deeper:

const deepCart = structuredClone(originalCart); // modern, reliable
Enter fullscreen mode Exit fullscreen mode

Array.sort() is a mutation trap too

const scores = [50, 20, 80, 10];
const sorted = scores.sort((a, b) => a - b);

console.log(scores); // [10, 20, 50, 80] — original got sorted!
console.log(scores === sorted); // true, same array
Enter fullscreen mode Exit fullscreen mode

sort(), reverse(), and splice() mutate in place. map(), filter(), and slice() don't. Memorize that split — it'll save you a debugging session eventually.

The Comparison Table You'll Bookmark

Behavior PHP Python JavaScript
Assignment Copy Reference Reference
Function param Copy Reference Reference
Force reference &$arr N/A N/A
Shallow copy default arr[:] / list(arr) [...arr]
Deep copy unserialize(serialize($arr)) copy.deepcopy() structuredClone()
In-place sort sort() .sort() .sort()
Sorted copy $c = $arr; sort($c); sorted(arr) [...arr].sort()

How to Actually Debug This When It Happens

  1. Print the array right after creation — confirm it's correct at the source.
  2. Check every assignment — copy or reference? Default assumption: PHP copies, Python/JS share.
  3. Audit functions that touch the array for in-place mutators (.push(), .append(), .sort(), direct index writes).
  4. Add explicit copy semantics where intent was "don't touch the original."
  5. If nested data is involved and a shallow copy didn't fix it, go deep.

The One-Line Takeaway

PHP makes you opt into sharing. Python and JavaScript make you opt into copying. Once that flips in your head, this entire category of bug stops being mysterious.

Found this helpful? Check out more at codepractice.in

Top comments (0)