Learn by reading through in order

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.

Immutable arguments
callerx = 5in functionx = 5x = 5(unchanged)x = 15(new value)passx += 10
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

Write a tax-conversion function and confirm that changing the int argument doesn't affect the outside.

① Define def add_tax(price):, set price = int(price * 1.1), then print f"inside: {price}". (No return value this time.)

② Set base_price = 1000, call add_tax(base_price), then print f"outside: {base_price}".

Success means the outer base_price is unchanged.

(The explanation appears once it runs correctly.)

Python Editor

Run code to see output

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.

Mutable arguments
callermy_list = [1, 2, 3]in functionitems = [1, 2, 3]my_list =[1, 2, 3, 100](changed too)items =[1, 2, 3, 100]pass the same boxitems.append(100)reflected
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.

Feel firsthand how outer data gets unintentionally modified.

① Define def add_item(cart, item):, run cart.append(item), then print f"inside: {cart}". (No return.)

② Prepare my_cart = ["milk", "bread"], call add_item(my_cart, "eggs"), then print f"outside: {my_cart}".

Confirm that "eggs" ends up in the outer my_cart too (this is the difference from the immutable case in the previous section).

Python Editor

Run code to see output

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.

Make a new box with .copy() before modifying
items = cart.copy()items.append(x)the original cartstays the sameno effect
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

Rewrite the function from the previous section into a safe version that doesn't modify the caller.

① Define def add_item_safely(cart, item):, copy the input with items = cart.copy(), run items.append(item), and finish with return items.

② Prepare my_cart = ["milk", "bread"] and capture the result with new_cart = add_item_safely(my_cart, "eggs").

print() both my_cart and new_cart and confirm that the original my_cart is unchanged while only new_cart has "eggs" added.

Python Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1After running this code, what's the value of x?
def f(value):
value += 10

x = 5
f(x)
print(x)

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?