ProgrammingPython Developer / Data Engineer

What happens when a mutable object (for example, a list or dictionary) is passed to a Python function? How can unexpected changes inside and outside the function be avoided?

Pass interviews with Hintsage AI assistant

Answer.

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:

  • Passing a mutable object allows its state to be changed both inside and outside the function.
  • To avoid this, data copying (shallow/deep copy) is used.
  • Immutable objects are protected from such changes.

Trick Questions.

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]

Common mistakes and anti-patterns

  • Modifying the passed mutable object inside the function without notifying the user.
  • Using shallow copying for nested data structures (an error with mutating nested objects).
  • Using mutable objects as default values for function arguments.

Real-life example

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.