Python Reference Passing: Mutable vs Immutable Use Cases

1. Difference Between Passing by Value and Passing by Reference

In Python, there are two ways to pass arguments to functions: pass-by-value and pass-by-reference.
  • Pass-by-value: A method where a copy of the value is passed to the function as an argument; modifying the parameter inside the function does not affect the original variable.
  • Pass-by-reference: A method where a reference (address) to the variable is passed to the function, so changes made inside the function are reflected in the original variable.
In Python, this behavior varies depending on the nature of the object. Python’s “pass-by-reference” behavior especially applies to mutable data types and can significantly affect how code behaves, so it’s important to understand it correctly.

2. Characteristics of Objects in Python

In Python, all data are treated as objects, and based on their object characteristics they are classified as either immutable (cannot be changed) or mutable (can be changed). This distinction affects how they behave when passed as arguments to functions.
  • Immutable objects: objects that cannot be modified after creation. Examples include integers (int), floating-point numbers (float), strings (str), tuples (tuple). Because these data types cannot be changed once created, operations on them inside a function do not affect the caller even when passed by reference.
  • Mutable objects: objects that can be modified after creation. Examples include lists (list), dictionaries (dict), sets (set). Because changes to these data types within a function are reflected in the caller, they are especially affected by Python’s reference-passing behavior.
RUNTEQ(ランテック)|超実戦型エンジニア育成スクール

3. How Python Passes Arguments

In Python, when you pass arguments to a function, references to objects are passed. This behavior is sometimes called “pass-by-object-reference.” From here, we’ll walk through concrete examples that illustrate Python’s behavior depending on whether objects are immutable or mutable.

3.1 Immutable Objects

When you pass an immutable object to a function, assigning a new value inside the function creates a new object and does not affect the original object.

Example: Passing an Immutable Integer

def modify_number(num):
    num = num * 2
    print("Number inside the function:", num)

original_num = 5
modify_number(original_num)
print("Number outside the function:", original_num)
Output:
Number inside the function: 10
Number outside the function: 5
Explanation

Inside the modify_number function, the variable num is doubled, but this does not affect original_num outside the function. Since integers are immutable, a new object is created when a new value is assigned, and original_num remains unchanged.

3.2 Mutable Objects

On the other hand, when a mutable object is passed to a function, its reference is also used inside the function. As a result, changes made within the function are reflected in the original object as well.

Example: Passing a Mutable List

def add_item(my_list):
    my_list.append("Added element")
    print("List inside the function:", my_list)

original_list = ["Element 1", "Element 2"]
add_item(original_list)
print("List outside the function:", original_list)
Output:
List inside the function: ['Element 1', 'Element 2', 'Added element']
List outside the function: ['Element 1', 'Element 2', 'Added element']
Explanation

Since lists are mutable, the changes made inside the add_item function are directly reflected in original_list outside the function. In this way, mutable objects are shared between inside and outside the function, so modifications within the function also affect the caller.

4. Cautions and Countermeasures for Passing by Reference

Passing mutable objects to functions in Python can lead to unexpected behavior. Here are some countermeasures to avoid such issues.

4.1 Use Copies of Objects

If you want to modify an object inside a function, you can avoid affecting the original by copying it before making changes. In Python, you can use the copy module to create shallow copies (copy.copy) or deep copies (copy.deepcopy).

Example: Using Deep Copy to Avoid Mutable Object Issues

import copy

def add_item(my_list):
    my_list_copy = copy.deepcopy(my_list)
    my_list_copy.append("Added element")
    print("List inside the function (copy):", my_list_copy)

original_list = ["Element 1", "Element 2"]
add_item(original_list)
print("List outside the function:", original_list)
Output:
List inside the function (copy): ['Element 1', 'Element 2', 'Added element']
List outside the function: ['Element 1', 'Element 2']
Explanation By using deepcopy, a new object is created inside the function, leaving the original list unaffected. This allows independent changes inside and outside the function.

4.2 Use None for Default Arguments

Avoid setting mutable objects as function default arguments; it’s recommended to use None as the default and create a new object inside the function.

Example: Safe Way to Make the Default Argument None

def add_item(my_list=None):
    if my_list is None:
        my_list = []
    my_list.append("New element")
    print("List inside the function:", my_list)
    return my_list

# When no list is passed
result = add_item()
print("Returned list:", result)

# When a list is passed
existing_list = ["Element 1", "Element 2"]
result = add_item(existing_list)
print("Returned list:", result)
print("Original list:", existing_list)
Output:
List inside the function: ['New element']
Returned list: ['New element']
List inside the function: ['Element 1', 'Element 2', 'New element']
Returned list: ['Element 1', 'Element 2', 'New element']
Original list: ['Element 1', 'Element 2', 'New element']
Explanation By using the default argument None and generating a new list inside the function, you can prevent external lists from being unexpectedly modified. Setting None as the default argument makes clear whether to create a new list or use the one provided.
侍エンジニア塾

5. Practical Example: Code to Deepen Understanding of Pass-by-Reference

Let’s review what we’ve covered with a practical example.

Example: Modifying a Dictionary’s Keys and Values

Dictionaries are mutable objects, so operations performed inside a function affect the dictionary outside the function. The example below shows the behavior when changing a dictionary’s keys and values.
def modify_dict(my_dict):
    my_dict["new key"] = "new value"
    print("Dictionary inside the function:", my_dict)

original_dict = {"key1": "value1", "key2": "value2"}
modify_dict(original_dict)
print("Dictionary outside the function:", original_dict)
Output:
Dictionary inside the function: {'key1': 'value1', 'key2': 'value2', 'new key': 'new value'}
Dictionary outside the function: {'key1': 'value1', 'key2': 'value2', 'new key': 'new value'}
Explanation Because dictionaries are mutable, additions made inside the function are also reflected in the original_dict outside the function. Like lists, dictionaries can affect the caller because they are passed by reference.

6. Summary

Python’s “pass-by-reference” behaves differently depending on an object’s characteristics, so understanding it is necessary to improve your code’s reliability. In particular, to avoid unexpected behavior from passing references to mutable objects, it’s advisable to make use of copying and default argument None.

Summary Points

  • Difference between pass-by-value and pass-by-reference: In Python, whether modifications inside a function affect the original variable depends on the object’s nature.
  • Immutable vs. mutable: Immutable objects in Python do not reflect changes made inside a function, whereas mutable objects are shared between inside and outside the function via reference passing.
  • Mitigation strategies: For mutable objects, methods such as using deepcopy from the copy module, or setting default arguments to None, are effective to avoid unexpected behavior.
I hope this article has deepened your understanding of reference passing in Python. Use this knowledge to implement efficient, less error-prone code.