Understanding how Python passes arguments to functions is crucial to prevent unexpected data changes and to design code correctly.
In traditional programming languages like C or Java, arguments are passed by value (copy by value) or by reference (copy by reference). However, Python uses a different model — call by object reference (sometimes called "call by sharing").
Many developers mistakenly believe that Python always passes arguments by reference or by value. This inevitably leads to situations where mutable objects are unexpectedly modified in the calling code.
In Python, the values of function parameters are references to the objects that are passed to the function. This means:
Example:
# list - mutable (mutable) def add_item(lst): lst.append(42) my_list = [1, 2, 3] add_item(my_list) print(my_list) # [1, 2, 3, 42] # int - immutable (immutable) def add_num(x): x = x + 1 num = 10 add_num(num) print(num) # 10
Key Features:
Are arguments always passed by reference in Python?
No, in Python, references to objects are passed, and how an object behaves depends on whether it is mutable or not. An immutable object will create a new object upon any modification.
Can a function reassign a mutable argument to affect the external object?
No. If you assign a new value to a parameter inside a function, the external object will not change — you are only changing the local reference.
Example:
def reassign_list(lst): lst = [99, 100] my_list = [1, 2, 3] reassign_list(my_list) print(my_list) # [1, 2, 3]
Why might a function that accepts a list by default behave strangely on subsequent calls?
Because the default value is created once — at the function definition, and if it is modified (for example, by adding an element), it will change for all subsequent calls.
def add_element(x, cache=[]): cache.append(x) return cache print(add_element(1)) # [1] print(add_element(2)) # [1, 2]
A programmer passes a list to a function and expects that the original list will not change, but the function adds an element.
Pros:
Cons:
A programmer explicitly copies the list inside the function if they need to return something but not change the original:
def process_data(data): data = data.copy() # or list(data) # safe operation with a copy data.append('report') return data
Pros:
Cons: