DEV Community

pickuma
pickuma

Posted on • Originally published at pickuma.com

Pass by Value vs Pass by Reference

"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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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).

In Python and Java, mutation is visible to the caller; reassignment is not. If you change the object the parameter points to, the caller sees it. If you point the parameter at a different object, only your local copy moves. Saying these languages are "pass by reference" is the most common mistake here — they pass a reference by value.

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.


Originally published at pickuma.com. Subscribe to the RSS or follow @pickuma.bsky.social for new reviews.

Top comments (0)