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.