Background:
Parameter passing in Python implements the principle of "call by object reference" (sometimes called call by sharing). This means that the variable inside the function starts pointing to the same object in memory as the argument passed from the outside.
Problem:
If the function modifies the passed mutable object (for example, a list or dictionary), the changes are visible outside the function as well. This can lead to elusive bugs, especially if it is expected that the function will not modify the input data.
Solution:
To avoid side effects, you should make a copy of the object inside the function or use immutable data structures. For copying, standard methods are used (for example, list.copy() for lists, dict.copy() for dictionaries, or copy.deepcopy()).
Code example:
def append_one(xs): xs.append(1) return xs lst = [0] append_one(lst) print(lst) # [0, 1] # How to avoid changes? Make a copy: def safe_append_one(xs): ys = xs.copy() ys.append(1) return ys lst2 = [0] safe_append_one(lst2) print(lst2) # [0]
Key features:
Can you be sure that the copy of the list through .copy() is completely independent of the original list?
No — .copy() creates a shallow copy. If there are nested mutable objects inside, changes in them will be visible in the original as well.
import copy lst = [[1, 2], [3, 4]] shallow = lst.copy() shallow[0][0] = 42 print(lst) # [[42, 2], [3, 4]] deep = copy.deepcopy(lst) deep[0][0] = 100 print(lst) # [[42, 2], [3, 4]]
Is returning a new object based on the input a guarantee that the original won't be changed?
Not always. If the new object contains parts of the original (for example, a reference to an inner nested list), the original object may be changed.
def duplicate_list(xs): return xs * 2 lst = [[1], [2]] res = duplicate_list(lst) res[0][0] = 999 print(lst) # [[999], [2]]
Can default arguments for mutable objects lead to problems when calling the function multiple times?
Yes — the default value is evaluated only once when the function is defined.
def add_item(item, container=[]): container.append(item) return container print(add_item(1)) # [1] print(add_item(2)) # [1, 2]
Negative case
In a configuration processing library, a list was used as a default value, which led to accumulation of elements between different calls of the function. The behavior was unpredictable and took a long time to identify.
Pros:
Less code for repeated calls, visible memory savings.
Cons:
Implicit behavior, difficulties in debugging, long-term errors.
Positive case
Using None as a default value and explicitly creating a new object for each call.
Pros:
Predictability, absence of unexpected side effects, reliability.
Cons:
Requires awareness and a bit more code.