Q1After running this code, what's the value of x?def f(value):
value += 10
x = 5
f(x)
print(x)
Function Arguments and Object References — Mutables Get Modified Inside Functions
See how Python function arguments behave with mutable vs immutable types and dodge the classic gotchas in your browser.
When you call a function, the behavior changes depending on whether the argument is a mutable type (list / dict / set) or an immutable type (int / str / tuple). This article walks through that difference and the basic patterns for writing safe functions.
Immutable arguments — changes inside don't leak out
Integers, floats, strings, and tuples are immutable (can't be changed in place). Receive one as an argument and rewrite it with += 10 or = some_other_value, and the caller's variable is untouched.
Under the hood, += 10 creates a new value and rebinds only the inner argument name — the caller's name still points to the original.
def try_modify_number(value):
value += 10
print(f"inside: {value}")
x = 5
try_modify_number(x)
print(f"outside: {x}")
# inside: 15
# outside: 5 <- still the original
# Same story for strings and tuples
def try_modify_text(text):
text = text + " world"
print(f"inside: {text}")
message = "hello"
try_modify_text(message)
print(f"outside: {message}") # hello
Mutable arguments — changes inside leak outside
On the other hand, when you pass a mutable type like list / dict / set and call something like .append() or .update() to modify the contents in place, the caller's variable sees the same change.
That's because the moment the argument is passed, the caller's name and the inner argument name share the same box. It's the same mechanism as y = x from the earlier mutable-vs-immutable article — now happening at the call boundary.
def try_modify_list(items):
items.append(100)
print(f"inside: {items}")
my_list = [1, 2, 3]
try_modify_list(my_list)
print(f"outside: {my_list}")
# inside: [1, 2, 3, 100]
# outside: [1, 2, 3, 100] <- the outside changed too
# Same with dicts
def set_role(user, role):
user["role"] = role
admin = {"name": "Alice"}
set_role(admin, "admin")
print(admin) # {'name': 'Alice', 'role': 'admin'}
When inner mutations leak out, the cause is hard to track
cart.append(...) looks perfectly intentional on its own line. But because the append inside the function also reaches the outer my_list, you can end up with "the list silently grew" symptoms that surface somewhere completely unrelated.
Modifying only inside the function — protect arguments with .copy()
When you want to leave the caller's data alone and only change things inside the function, just put .copy() at the top of the function. Copy the incoming list, dict, or set into a separate box before modifying it, and the caller is unaffected.
Return the modified version with return, and the caller can hold both the original cart and the new cart at once. Once this pattern is muscle memory, you'll be writing safe functions free of side effects (no caller modification).
The full flow: ① copy the argument with .copy() into a separate box → ② edit the copy → ③ return the result. Memorize these three steps and you can safely design any function that takes mutable arguments.
def add_item_safely(cart, item):
items = cart.copy() # copy into a separate box before editing
items.append(item)
return items # send the result back via return
my_cart = ["milk", "bread"]
new_cart = add_item_safely(my_cart, "eggs")
print(my_cart) # ['milk', 'bread'] <- untouched
print(new_cart) # ['milk', 'bread', 'eggs'] <- a separate list
Knowledge Check
Answer each question one by one.
Q2After running this code, what's the value of my_list?def g(items):
items.append(100)
my_list = [1, 2, 3]
g(my_list)
print(my_list)
Q3When you want to return a new list without modifying the caller's list, what's the first thing you should do?