Skip to content
Innovialabs

Pass by Value vs Pass by Reference: How Arguments Work

Basics4 min

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

LanguageDefaultReal by-reference option
PythonSharing (everything is an object)None
JavaScriptValue for primitives, sharing for objectsNone
JavaValue for primitives, sharing for objectsNone
GoValue, always; slices and maps carry internal pointersPointers (*T)
C#Value for structs, sharing for classesref, 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 append surprises. 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.
LanguageShallow copyDeep copy
Pythonlist(x), x.copy(), copy.copy(x)copy.deepcopy(x)
JavaScript[...arr], {...obj}, Object.assignstructuredClone(obj)
Javanew ArrayList<>(list), copy constructorsManual copy or serialization
Gocopy(dst, src), maps.CloneManual 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.freeze for 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.

FAQ

No. Java always passes by value. For objects, the value is a reference, so a method can change the object's fields but cannot make the caller's variable point to another object.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.