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

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

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

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
}

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

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']

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

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!

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

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

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']

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

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

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

Fix it by copying first:

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

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

const deepCart = structuredClone(originalCart);  // modern, reliable

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

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

  • Print the array right after creation - confirm it's correct at the source.
  • Check every assignment - copy or reference? Default assumption: PHP copies, Python/JS share.
  • Audit functions that touch the array for in-place mutators (.push(), .append(), .sort(), direct index writes).
  • Add explicit copy semantics where intent was "don't touch the original."
  • 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

Comments

No comments yet. Start the discussion.