Pass by Value vs Pass by Reference: How Arguments Work
Basics4 minInnovialabs editorial team
In short
Most popular languages pass a copy of the value, and for objects that value is a reference, so a function can mutate a shared object but cannot rebind the caller's variable; true pass by reference exists only where the language says so explicitly, like ref in C# or pointers in Go.
The short answer
There are three models you will meet:
- Pass by value — the function gets a copy. Changing it does not affect the caller.
- Pass by reference — the function gets an alias to the caller's variable. Assigning to the parameter changes the caller's variable.
- Pass by sharing (also called "pass object reference by value") — the function gets a copy of a reference to the same object. It can mutate that object, but reassigning the parameter only changes the local copy.
Python, JavaScript and Java use the third model for objects. That is why "Java is pass by reference" is a common but wrong statement.
Language by language
| Language | Default | Real by-reference option |
|---|---|---|
| Python | Sharing (everything is an object) | None |
| JavaScript | Value for primitives, sharing for objects | None |
| Java | Value for primitives, sharing for objects | None |
| Go | Value, always; slices and maps carry internal pointers | Pointers (*T) |
| C# | Value for structs, sharing for classes | ref, out, in |
Python and JavaScript
def update(items):
items.append(4) # mutates the shared list: caller sees it
items = [0] # rebinds the local name: caller does not see it
data = [1, 2, 3]
update(data)
print(data) # [1, 2, 3, 4]
JavaScript behaves the same way with objects and arrays. Strings and numbers are immutable, so a function can never change them in place in either language.
Java
A method receives a copy of the reference. list.add(x) changes the caller's list; list = new ArrayList<>() does not. There is no way to make a Java method replace the caller's variable.
Go
Go copies everything, including structs. To let a function modify a struct, pass a pointer:
func rename(u *User) { u.Name = "Ann" }
Slices and maps are small headers that point to shared data. Changing s[0] inside a function is visible to the caller, but append may allocate a new array, and that change is not visible unless you return the slice.
C#
Structs are copied, class instances are shared. The ref keyword gives true pass by reference: assigning to a ref parameter changes the caller's variable. out is for returning extra values, and in passes a read-only reference, which avoids copying large structs.
Typical mutation bugs
- Unexpected side effects. A helper "cleans" a list by removing items, and the original data used elsewhere changes too.
- Python mutable default arguments. The default object is created once and shared across calls:
def add_tag(tag, tags=[]): # bug: one list for all calls
tags.append(tag)
return tags
def add_tag(tag, tags=None): # fix
tags = [] if tags is None else tags
tags.append(tag)
return tags
- Shared state in UI code. In React and similar libraries, mutating state in place means the reference does not change, so the update may not be detected.
- Go
appendsurprises. Two slices can share one underlying array and overwrite each other's data.
Copying: shallow vs deep
When you need an independent object, copy it — but know how deep the copy goes.
- Shallow copy creates a new outer container, while nested objects are still shared.
- Deep copy recursively copies everything, so nothing is shared.
| Language | Shallow copy | Deep copy |
|---|---|---|
| Python | list(x), x.copy(), copy.copy(x) | copy.deepcopy(x) |
| JavaScript | [...arr], {...obj}, Object.assign | structuredClone(obj) |
| Java | new ArrayList<>(list), copy constructors | Manual copy or serialization |
| Go | copy(dst, src), maps.Clone | Manual copy |
| C# | MemberwiseClone, new List<T>(list) | Manual copy or serialization |
A shallow copy of a list of dictionaries still shares the dictionaries. That is the most common source of "I copied it, but it still changed".
How to avoid the bugs
- Do not mutate arguments unless the function name makes it obvious (
sort_in_place,appendTo). - Return new values instead of changing inputs; it makes code easier to test.
- Prefer immutable data where the language supports it: tuples,
frozenset,readonly, records,Object.freezefor shallow protection. - Copy at the boundary — when data enters a module or is stored, copy it once.
- Deep copy deliberately. It is slower and can break on objects with external resources, so use it only where you really need full independence.