Pass by Value vs Pass by Reference: The Concept Every Interview Tests

June 5, 20268 min read
dsaalgorithmsinterview-prepdata-structures
Pass by Value vs Pass by Reference: The Concept Every Interview Tests
TL;DR
  • Pass by value gives the function its own copy of the argument; the caller's original data is untouched no matter what
  • Pass by reference shares the original object; mutations inside the function are visible to the caller
  • Python uses pass by object reference: reassigning a parameter is invisible to the caller, but calling .append() or mutating a key modifies the shared object
  • The backtracking copy bug (result.append(path) instead of path[:]) is the most common interview manifestation — every result entry ends up empty because they all reference the same list
  • Aliasing (b = a, [[]]*n) silently creates shared objects that change together — recognizing this under pressure is a strong interviewer signal
  • When you claim O(1) extra space, verify you haven't secretly copied the input inside the function via list(), sorted(), or slicing

You wrote the backtracking solution. It looked right. You ran it and got [[], [], []]. Every sublist is empty, even though you watched yourself push items in. You stare at the screen. The interviewer stares at you. You both know something is deeply wrong, and exactly one of you knows what it is.

The underlying issue is pass by value vs pass by reference. One of those concepts that sounds like a third-semester textbook chapter until it silently corrupts your output in a live interview.

Pass by Value vs Pass by Reference: What Actually Gets Passed

Pass by value means the function receives a copy of the argument. Whatever the function does with that copy stays in the function. The caller's original variable is untouched.

def double(x): x = x * 2 return x n = 10 double(n) print(n) # 10 - unchanged

Pass by reference means the function receives a pointer to the original memory location. Changes inside the function are visible to the caller because both are looking at the same data.

void double_it(int& x) { x = x * 2; } int n = 10; double_it(n); // n is now 20 - the original was modified

The difference comes down to one question: does the function share memory with the caller, or does it work on its own copy?

Why Python (and JavaScript, Java) Confuse Everyone

Python doesn't do either cleanly. The model is called "pass by object reference," sometimes "pass by assignment," and always described in a way that requires at least two reads. The reference itself is passed by value, but the object the reference points to is shared.

That sounds like it was written by a committee that couldn't agree on either option. But it has a concrete consequence. Mutating the object (calling .append(), updating a dict key) is visible to the caller because the object is shared. Reassigning the variable (pointing it at a new object) is not visible, because you just changed your local copy of the reference.

def add_item(lst): lst.append(4) # mutates the shared object - caller sees this def replace_list(lst): lst = [99, 100] # rebinds the local reference - caller doesn't see this nums = [1, 2, 3] add_item(nums) print(nums) # [1, 2, 3, 4] replace_list(nums) print(nums) # [1, 2, 3, 4] - completely unchanged

Here's what's actually happening in memory when Python makes that function call:

Python pass by object reference: mutation vs rebinding

Mutate the object and both frames see it. Reassign the variable and only the local frame knows. Immutable types (int, str, tuple, float) sidestep the whole issue: you can't call .append() on an integer, so any operation that looks like a modification creates a new object instead.

JavaScript is identical to Python here. Java works the same way for objects, with primitives always copied by value.

Where Each Language Falls

LanguagePrimitivesObjects / Collections
PythonValue (immutable anyway)Reference by value
JavaScriptValueReference by value
JavaValueReference by value
C++Value by defaultValue by default; & opts in to reference
GoValueValue; *T pointer for reference semantics
RustMove (ownership transfer)Move; &T / &mut T for borrowing

C++ is the most explicit: you decide at the call site and function signature whether you're sharing or copying. Python and JavaScript give you reference semantics automatically for any object, whether you wanted it or not.

How This Shows Up in Your Space Claim

When you pass a large data structure by value (or make a copy), you pay O(n) extra space for that copy. Passing by reference costs O(1) because you're only moving a pointer.

When you claim O(1) extra space, make sure you're not secretly copying a list somewhere. A function that creates sorted_nums = list(nums) inside has already spent O(n) space, and saying "constant space" right after is going to get circled in the write-up.

In recursive algorithms, each frame of the call stack holds its own local variables. If you pass an object by reference, all recursion frames share the same object, which is usually what you want for efficiency but is exactly what causes the classic backtracking bug.

The Bug That Keeps Appearing in Interviews

Backtracking is where this causes the most pain. The pattern: build a path, recurse, undo the last choice. When you find a valid solution, you add the current path to your results.

The buggy version:

def subsets(nums): result = [] path = [] def backtrack(start): result.append(path) # adds a reference to path, not a copy for i in range(start, len(nums)): path.append(nums[i]) backtrack(i + 1) path.pop() # undoes the choice backtrack(0) return result

Every entry in result points to the same list object. By the time the recursion finishes, path has been emptied by the final series of pop() calls. You have a list of ghost pointers. They all agree on one thing: there's nothing here. result comes back as [[], [], [], ...].

Me debugging this exact output at 2am

The bug that appears correct on every read-through, because the logic IS correct. The memory model just disagrees.

The fix is one character wide:

result.append(path[:]) # creates a shallow copy at this moment in time

Now each entry captures the current state of the path, independent of what happens to the original. For deeper structures (a list of lists), use copy.deepcopy(path). The post Pass by Value vs Reference in Python walks through why Python's model makes this necessary across multiple backtracking examples.

Two More Ways This Bites You

Mutation you didn't intend. You pass a list to a helper function to "just read" it. The helper sorts it in place. Now your caller's list is sorted, and you have no idea why, because you definitely didn't touch it.

def find_median(nums): nums.sort() # mutates the caller's list! mid = len(nums) // 2 return nums[mid] data = [5, 1, 3, 2, 4] find_median(data) print(data) # [1, 2, 3, 4, 5] - sorted, not what you expected

Fix: sort a copy with sorted(nums), or document the mutation explicitly so the next person reading your code doesn't spend twenty minutes confused.

Aliasing. Two variables point to the same object when you thought they were independent.

a = [1, 2, 3] b = a # b is NOT a copy, it's an alias b.append(4) print(a) # [1, 2, 3, 4]

This shows up in graph problems where you initialize adjacency lists with [[]]*n. Every inner list is the same object. Append to one and you append to all of them. This gets roughly 100% of people the first time they hit it.

# Wrong - all n inner lists are the SAME list adj = [[]] * n # Right - n independent lists adj = [[] for _ in range(n)]

Mutation and aliasing are on the explicit checklist in Debugging in a Coding Interview because they're the hardest bugs to catch under pressure. The code looks fine. The logic looks fine. The memory model is just quietly doing something different from what you pictured.

What Interviewers Are Watching For

Do you know to copy before mutating shared state? If you sort an input array, do you say "I'm doing this in place, which modifies the input" or do you silently mutate and move on? Naming that you're about to mutate shared data is a positive signal, not an obvious one.

Can you debug an aliasing problem? If your output looks wrong and everything else checks out, aliasing belongs on your short list. Interviewers who see you reason through "wait, are these two things pointing to the same object?" know you understand how memory works.

When you analyze space complexity, do you account for copies you create? Saying "this is O(1) space" when you're copying the input inside the function is an error that sticks in the write-up. See Coding Interview Mistakes for how those write-up entries actually affect decisions.

The real skill is reasoning about who owns data and whether two names refer to the same thing. You build that by thinking through these bugs, not by memorizing that Python is "pass by object reference."

Practice Explaining It, Not Just Fixing It

When you write path[:], do you say "this creates a shallow copy because the original path gets emptied by backtracking"? Or do you write it silently and hope the interviewer connects the dots?

Talking through your memory model out loud is exactly the kind of communication that's hard to train alone. SpaceComplexity runs voice-based mock interviews with rubric feedback that scores how clearly you explain your reasoning, not just whether your code compiles.

Further Reading