Pass by Value vs Pass by Reference
Why mutating an object inside a function works in Python and Java but reassigning the parameter doesn't — and how that differs from C copies and C++ references.
“Is this language pass by value or pass by reference?” sounds like a yes/no question, but it trips up experienced developers because the honest answer for most popular languages is “neither, exactly.” The confusion is worth clearing up once, because it explains a whole class of bugs.
The two real mechanisms
Pass by value means the function receives a copy of the argument. Whatever the function does to its parameter, the caller’s original variable is unaffected, because they are two separate pieces of memory.
Pass by reference means the function receives an alias — another name for the caller’s variable itself. Assigning to the parameter assigns to the caller’s variable. There is only one piece of memory, under two names.
C is strictly pass by value. When you pass an int, the function gets a copy:
void tryToChange(int x) { x = 99; } // local copy only
int a = 1;
tryToChange(a); // a is still 1
To let a C function change the caller’s variable, you pass a pointer — the value of an address — and dereference it. That is still pass by value (you copied the pointer), but the copied address points back at the original. C++ adds true pass by reference with the & syntax: void change(int& x) makes x a genuine alias, so x = 99 updates the caller’s variable directly.
Why Python and Java confuse everyone
Python and Java are pass by value. The catch is what the value is: for objects, the value is a reference (an object identity / handle), and that reference is copied into the parameter. So the function and the caller now hold two separate references that point at the same object.
This produces the behavior that looks like two contradictory rules:
def demo(lst):
lst.append(4) # MUTATES the shared object -> caller sees [1, 2, 3, 4]
lst = [99] # REBINDS the local name only -> caller unaffected
data = [1, 2, 3]
demo(data)
print(data) # [1, 2, 3, 4]
The append reaches through the reference and modifies the one object both names point to, so the change is visible to the caller. The assignment lst = [99] only points the local copy of the reference at a new object; the caller’s reference still points at the original list. Java behaves identically: mutate a passed object’s fields and the caller sees it; reassign the parameter and the caller does not.
This is why the precise name is pass by value of a reference, sometimes called pass by object sharing or call by sharing. It is neither classical pass by reference (because reassignment does not propagate) nor a deep copy (because mutation does propagate).
Practical consequences
This distinction is not pedantry; it predicts real behavior. A function that “clears” a list by writing lst = [] silently does nothing to the caller — you wanted lst.clear(). A function that takes a config dict and adds defaults via config["timeout"] = 30 will leak those defaults back to every caller unless you copy first. Immutable types (Python’s int, str, tuple; Java’s primitives and String) sidestep the whole issue: since you can’t mutate them, the only way to “change” one is to reassign, which never escapes the function.
The mental model to keep: assignment in these languages binds a name to an object; it never copies the object and never copies the caller’s binding. Everything else follows.
FAQ
Is Java pass by reference for objects?
How do I actually change a caller's variable in Python?
Does passing a large object copy all its data?
Related reading
2026-06-22
TCP vs UDP, Explained Through What Breaks When You Pick Wrong
TCP and UDP aren't interchangeable. We walk through the exact failure modes — head-of-line blocking, silent packet loss, Nagle delays — that show up when you pick the wrong transport.
2026-06-22
Write-Ahead Logging: How Databases Survive a Power Cut
How write-ahead logging keeps your data intact when the machine dies mid-write — the log-first rule, fsync, checkpoints, and why PostgreSQL and SQLite both rely on it.
2026-06-22
Backpressure, Explained Through a Queue That Won't Fall Over
What backpressure actually is, why an unbounded queue is a memory leak in disguise, and the four strategies a producer can take when a consumer falls behind.
2026-06-22
What a Bloom Filter Actually Saves You (and When It Lies)
A bloom filter trades a small false-positive rate for big memory savings. Here is the math behind the trade, where it pays off, and the failure mode that bites people.
2026-06-22
Idempotency, Explained Through the Retry That Doesn't Double-Charge
A practical look at idempotency keys: why a retried payment request shouldn't charge a card twice, how the pattern works, and where it quietly breaks in production.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.