The idea: what gets copied when you pass a value

A call f(a) always copies something into the new stack frame of f. The question is what. It can be the whole list, nothing at all (the parameter becomes another name for the variable a), or just the pointer that a holds. Those three answers are pass by value, pass by reference and the rule Java, Python and JavaScript use: pass the reference by value.

Three panels, each with a stack holding main's a and f's x, and a heap. By value: a points to list #1 and x points to its own copy #2. By reference: x has an arrow to the variable a, and a points to #1. Reference by value: a and x both point to the same list #1.
Inside f: x owns a copy, x is the variable a, or x holds a copy of a's pointer.

The canvas runs the same program in three panels at once, one hop per step. The left column of each panel is the stack: frame main with a (and b), and frame f with x while f runs. The right part is the heap: list objects numbered #1, #2, … in the order they were made. A blue arrow is a pointer to a list; a purple arrow is a reference to a variable. A grey box carries the value being passed or written.

Pass by value

In C++, void f(vector<int> x) takes its argument by value. The call runs the copy constructor: it reads every element of a's buffer #1 and writes them into a new buffer #2 that belongs to x. Whatever f then does to x stays in #2, and when f returns, x's destructor frees it. The caller's a prints [1, 2, 3] in every demo. The price is the copy: a million elements are a million writes, which is why C++ code passes big objects as const vector<int>&.

Pass by reference

void f(vector<int>& x) copies nothing. x becomes another name for the variable a itself; the compiler passes a's address and follows it on every use. So x[0] = 9 is a[0] = 9, and x = {7, 7} is a = {7, 7}: both change what the caller sees (Demo 1, Demo 2). C#'s ref, Pascal's var and PHP's &$x parameters work the same way.

Passing a reference by value (Java, Python, JS)

In Python, Java (for objects and arrays) and JavaScript (for objects), a variable never holds a list. It holds a reference: a pointer to an object on the heap. The call copies that pointer, so a and x are two names for one list #1. The language is still pass by value; the value just happens to be a pointer. That is why people argue about what to call it. Python's docs call it call by object reference, others say call by sharing.

Mutate vs reassign

The difference shows when f assigns to its parameter. Mutating goes through the pointer and reaches the shared list; assigning only re-aims the callee's copy of the pointer. In C++, = means something else again: it copies new contents into the object the name stands for.

f doesBy valueBy referenceReference by value
x[0] = 9 (Demo 1)a = [1, 2, 3]a = [9, 2, 3]a = [9, 2, 3]
x = [7, 7] (Demo 2)a = [1, 2, 3]a = [7, 7]a = [1, 2, 3]
x = [7, 7]; x[0] = 9 (Demo 3)a = [1, 2, 3]a = [9, 7]a = [1, 2, 3]

Demo 3 is the classic trap. In Python, after x = [7, 7] the name x points at a new list #2, so the next x[0] = 9 lands there, not in a. When f returns, nothing points at #2 any more: its reference count drops to 0 and it is freed. The same reason explains why a Java swap(a, b) method can never swap the caller's variables.

Shallow vs deep copy

Copying a list of lists has the same three answers one level down. Pick b = copy of a and the panels switch to a = [[1, 2], [3, 4]]: an outer list #1 whose two cells point at the inner lists #2 and #3.

Three panels. b = a: a and b both point to outer list #1, whose cells point to #2 and #3. Shallow copy: b points to a new outer list #4 whose cells still point to the same #2 and #3. Deep copy: b points to #4, whose cells point to new lists #5 and #6.
b = a shares everything, a shallow copy shares the inner lists, a deep copy shares nothing.
  • b = a copies nothing but the pointer. Every change through b is a change to a.
  • A shallow copy (a.copy(), list(a), a[:], copy.copy(a), JavaScript's [...a], Java's clone() on an array) makes one new outer list and copies its cells, which are pointers. So b[0][0] = 9 still changes a (Demo 4), while b[1] = [5, 6] only rewrites b's own outer list (Demo 5).
  • A deep copy (copy.deepcopy(a), JavaScript's structuredClone(a)) copies every list it reaches, so nothing is shared. It costs a copy of the whole structure and keeps a memo table so shared or cyclic parts are copied once.
Thenb = aShallow copyDeep copy
b[0][0] = 9 (Demo 4)a = [[9, 2], [3, 4]]a = [[9, 2], [3, 4]]a = [[1, 2], [3, 4]]
b[1] = [5, 6] (Demo 5)a = [[1, 2], [5, 6]]a = [[1, 2], [3, 4]]a = [[1, 2], [3, 4]]

Which language does what

LanguageDefaultReal pass by reference
CBy value (pass a pointer to share)No
C++By value, copy constructor runsT&
JavaBy value: primitives, and references to objectsNo
PythonReference by value (call by object reference)No
JavaScriptBy value: primitives, and references to objectsNo
C#By value (references for classes, copies for structs)ref, out
GoBy value (slices and maps carry a pointer inside)No

See also Stack vs Heap in Java for frames and references in the JVM, Stack vs Heap in C for what a stack frame and malloc look like underneath, and Python Garbage Collection for the reference counts that free #2 here.

What the page leaves out

Immutable values (Python ints, strings and tuples can't be mutated, so for them the three ways look the same), C++ move semantics and copy elision, const&, Rust's borrows, the vector's size and capacity fields, small-int caching, the cycle collector, real addresses, and objects with their own __copy__ / __deepcopy__ or copy constructors.