Interview Insights · Python
The Python Question That Ninety Percent of Candidates Fumble
Quick summary — Pass by assignment
Python is neither pass by value nor pass by reference in the classic sense; it passes object references by assignment. Candidates fumble this question because they try to pick one of two labels borrowed from other languages. Whichever label they choose, the interviewer can show a small piece of code where it gives the wrong prediction. The fix is a mental model, not a label: a function parameter is a new name bound to the same object the caller passed. Mutating that object is visible outside. Rebinding the name is not. This page explains the model, the code that tests it and the mutable default follow-up.
In this guide you will learn:
- Why both common labels fail when tested with real code
- The names-and-objects model that predicts every case
- Mutation versus rebinding, with examples you can run
- The mutable default argument follow-up, and its fix
- How to phrase the answer in under a minute
SECTION 01The question, and why most answers fall apart
I ask: 'If I pass a list into a function and the function changes it, does the caller see the change?' Many candidates say 'Python is pass by reference, so yes'. Then I show them a function that does lst = lst + [4] and ask what the caller sees. They predict the change is visible. It is not.
Other candidates say 'Python is pass by value', which fails the first test immediately, because append inside a function does change the caller's list. Each label works for one example and breaks on the other. That is the fumble: picking a label from C or Java instead of describing what Python actually does.
| Answer given | Predicts append correctly? | Predicts reassignment correctly? | Verdict |
|---|---|---|---|
| Pass by value | No, says caller is unchanged | Yes | Breaks on mutation |
| Pass by reference | Yes | No, says caller sees the new list | Breaks on rebinding |
| Depends on mutable or immutable | Partly | Partly | Close, but mixes two separate ideas |
| Pass by assignment (object reference) | Yes | Yes | Survives both tests |
def add_item(lst):
lst.append(4)
items = [1, 2, 3]
add_item(items)
print(items) # [1, 2, 3, 4] - caller sees the change
# 'Pass by value' predicts [1, 2, 3]. Wrong.def replace_items(lst):
lst = lst + [4] # builds a NEW list, rebinds the local name
print("inside:", lst)
items = [1, 2, 3]
replace_items(items)
print("outside:", items)
# inside: [1, 2, 3, 4]
# outside: [1, 2, 3] - caller unchanged
# 'Pass by reference' predicts [1, 2, 3, 4] outside. Wrong.
Two short functions, and each label fails one of them. When I see a candidate predict both correctly and explain why, I know they understand Python's object model, which affects how they will write and debug almost every function.
SECTION 02The model that predicts every case: names and objects
In Python, every value is an object, and variables are names attached to objects. When you call a function, Python attaches the parameter name to the same object the caller passed. Nothing is copied. From then on, two things can happen inside the function.
Mutation: you change the object itself, with append, update, item assignment or += on a list. Both names still point to that object, so the caller sees the change. Rebinding: you attach the local name to a different object, with plain =. The caller's name still points to the original object, so nothing changes outside. Immutable objects like ints, strings and tuples simply have no mutation methods, so only rebinding is possible.
| Operation inside function | Type | Caller sees change? | Why |
|---|---|---|---|
| lst.append(x) | Mutation | Yes | Changes the shared list object |
| lst += [x] | Mutation (for lists) | Yes | List += extends in place, then rebinds to the same object |
| lst = lst + [x] | Rebinding | No | + creates a new list; only the local name moves |
| n += 1 (int) | Rebinding | No | ints are immutable, so a new int object is created |
def show(lst):
print("param id:", id(lst))
lst.append("m") # mutation
print("after append:", id(lst))
lst = ["new"] # rebinding
print("after rebind:", id(lst))
data = ["a"]
print("caller id:", id(data))
show(data)
print(data) # ['a', 'm']
# caller id and param id are the same number.
# after append: still the same.
# after rebind: a different number.
# The caller only sees what happened BEFORE the rebind.def extend_list(lst):
lst += [99]
def extend_tuple(tup):
tup += (99,)
return tup
nums = [1, 2]
extend_list(nums)
print(nums) # [1, 2, 99] - list += mutates in place
pair = (1, 2)
result = extend_tuple(pair)
print(pair) # (1, 2) - tuples cannot mutate
print(result) # (1, 2, 99) - a new tuple was created
# Same operator, different behaviour, because the
# object's type decides whether += mutates or rebinds.
The += example is a favourite follow-up because it looks like a contradiction. It is not. For lists, += calls __iadd__, which extends the list in place. Tuples have no in-place version, so Python falls back to creating a new tuple and rebinding. The model still predicts both: ask whether the object changed or the name moved.
SECTION 03The follow-up: mutable default arguments
If you answer the first question well, expect this next: 'What is wrong with def add(item, bucket=[])?' It is the same model applied to defaults. A default value is created once, when the function is defined, not each time it is called. If the default is a mutable list and the function mutates it, every call that uses the default shares the same list.
Candidates who fumbled the first question usually fumble this one too. Candidates who understood the first one can often work out this answer even if they have never seen it, which is exactly what I hope to watch.
| Code | What candidates expect | What actually happens | Fix |
|---|---|---|---|
| def add(x, bucket=[]) | A fresh list each call | One list shared across calls | Use bucket=None, then create a list inside |
| def log(msg, when=datetime.now()) | Current time each call | Time fixed at definition | Default to None and call now() inside |
| def f(cfg={}) | Empty dict each call | One dict, changes persist | cfg=None, then cfg = {} if cfg is None |
| def f(n=0) | Fresh 0 each call | Fine: ints are immutable | No fix needed |
def add_tag(tag, tags=[]):
tags.append(tag)
return tags
print(add_tag("python")) # ['python']
print(add_tag("java")) # ['python', 'java'] - surprise!
print(add_tag("sql")) # ['python', 'java', 'sql']
print(add_tag.__defaults__)
# (['python', 'java', 'sql'],) - the default itself has growndef add_tag(tag, tags=None):
if tags is None:
tags = [] # a new list on every call
tags.append(tag)
return tags
print(add_tag("python")) # ['python']
print(add_tag("java")) # ['java']
mine = ["git"]
print(add_tag("docker", mine)) # ['git', 'docker']
# Spoken: "Defaults are evaluated once at definition.
# A mutable default is shared, so I use None as a
# sentinel and create the list inside the function."
Printing __defaults__ is a nice touch in an interview. It shows the default list sitting on the function object and growing, which proves the explanation rather than just stating it. In Uncodemy Python sessions, we use this exact demonstration because students remember it far better than the rule.
SECTION 04Practise the answer until it is automatic
Use these three sheets to make the model second nature. The goal is a calm one-minute answer, followed by confident predictions for any variation the interviewer shows you.
THE ONE-MINUTE ANSWER (say it aloud daily)
"Python passes arguments by assignment. The parameter
becomes a new name for the same object the caller
passed. No copy is made.
If the function mutates that object, like append on a
list, the caller sees the change. If the function
rebinds the name with =, only the local name moves,
and the caller is unaffected.
Immutable objects like ints and strings cannot be
mutated, so for them it behaves like pass by value."
Then offer: "I can show both cases in code."PREDICT THE CALLER'S VALUE AFTER EACH CALL
(function on the left, setup and call on the right)
1. def f(d): d["k"] = 1 | x = {}; f(x)
2. def f(d): d = {"k": 1} | x = {}; f(x)
3. def f(s): s += "!" | x = "hi"; f(x)
4. def f(l): l += [0] | x = [1]; f(x)
5. def f(l): l[:] = [9] | x = [1, 2]; f(x)
6. def f(t): t[0].append(5) | x = ([1],); f(x)
Answers:
1. {'k': 1} mutation
2. {} rebinding
3. 'hi' str is immutable, rebinding
4. [1, 0] list += mutates
5. [9] slice assignment mutates
6. ([1, 5],) the tuple holds a list, and the list mutatesFOLLOW-UPS TO EXPECT (answer each in 45 s)
[ ] What is wrong with def f(x, items=[])?
[ ] How do you avoid changing the caller's list?
(copy: lst = list(lst) or lst.copy() first)
[ ] Why does += behave differently for lists and tuples?
[ ] Can a function change an int it receives?
[ ] Is a tuple of lists immutable?
[ ] How would you make a function 'pure'?
Practise with a friend who changes one line of
the example each time and asks you to predict.
SECTION 05Habits that make this question easy
These habits turn a commonly fumbled question into one of your strongest moments.
- Drop the labels — Do not start with 'pass by value' or 'pass by reference'. Start with names and objects, then mention the labels only to compare.
- Always show two cases — One mutation example and one rebinding example together prove your model in a way a definition never can.
- Use id() to prove it — Printing ids before and after a change is a quick, convincing demonstration in any live round.
- Copy defensively when needed — If a function must not change its input, say you would copy it first. It shows you think about the caller.
- Know the default-argument fix — None as a sentinel is the standard pattern. Be ready to write it from memory.
SECTION 06Interview Q&A — Pass by assignment
Q1Is Python pass by value or pass by reference?
Neither in the classic sense. Python passes by assignment: the function parameter becomes a new name bound to the same object the caller passed. Mutating that object is visible to the caller. Rebinding the parameter with = only affects the local name.
Q2Why does appending to a list inside a function change the original?
Because the parameter and the caller's variable both refer to the same list object. append changes that object in place, so the change is visible through every name that refers to it.
Q3How can a function avoid modifying the list it receives?
Make a copy at the start, for example lst = list(lst) or lst.copy(), and work on that. For nested data, copy.deepcopy may be needed. Alternatively, return a new list instead of changing the input.
Q4What is the problem with mutable default arguments?
Default values are evaluated once, when the function is defined. A mutable default such as a list is shared by every call that uses it, so changes accumulate between calls. The standard fix is to default to None and create a new list inside the function.
Q5Why does += change a list in the caller but not a tuple?
For lists, += calls __iadd__, which extends the list in place, so the shared object changes. Tuples are immutable and have no in-place add, so += creates a new tuple and rebinds the local name, leaving the caller's tuple unchanged.
SECTION 07Test yourself — Pass by assignment quiz
Five questions. No sign-up.
0 / 5Pick an answer to see why it is right or wrong.
SECTION 08Frequently asked questions
Does Python pass arguments by reference?
Not exactly. Python passes object references by assignment. The function can change a mutable object it receives, but reassigning the parameter does not affect the caller's variable.
Why does my list change after calling a function in Python?
The function received a reference to the same list and changed it in place, for example with append, extend or item assignment. Copy the list first if the function should not affect the original.
What is a mutable default argument in Python?
It is a default parameter value like a list or dict. Because defaults are created once, a mutable default is shared between calls, and changes made in one call appear in later calls.
Are strings passed by value in Python?
Strings are passed the same way as everything else, by assignment. Because strings are immutable, a function cannot change the caller's string, so in practice it behaves like pass by value.
How do I explain pass by assignment in an interview?
Say that parameters are new names for the caller's objects, then show two tiny examples: append, which the caller sees, and reassignment, which the caller does not. Offer the mutable default as a related case.
How can I prepare for Python interview questions with a trainer?
Yes. Uncodemy's Python Training Course in Noida (classroom and online) includes live projects, one-to-one mock interviews and resume reviews, so you practise Python interview questions with trainer feedback before the real interview. You can book a free demo class from this page to see how the sessions work.
SECTION 09Related reads
Classroom & online · Noida
Master the Python fundamentals interviews test
Our Python Training Course covers Python's object model, data structures, functions and real projects, with mock rounds on the questions candidates fumble most.
Talk to a counsellor for current fees- Python object model in depth
- Prediction and tracing drills
- Mock technical rounds
- Weekday & weekend batches