Python Pass List to Function: Behavior and Pitfalls
python pass list to function: Learn how Python passes lists to functions, why mutation affects the caller, and how to avoid common pitfalls like mutable default argume...
When you call a function with a list in Python, the function receives a reference to the same list object, not a copy. This behavior is central to understanding how to safely write functions that accept lists. The phrase "python pass list to function" often confuses developers coming from languages with explicit pass-by-value or pass-by-reference semantics. In Python, the reference itself is passed by value, but the object it points to is shared. This means that modifying the list inside the function will affect the caller's list, but reassigning the parameter will not.
How Python Passes Lists to Functions
Consider a simple function that receives a list and prints its length:
def print_length(items): print(len(items)) my_list = [1, 2, 3] print_length(my_list)
When print_length is called, the local variable items is bound to the same list object that my_list references. No copy is made. This is true for all Python objects, but it matters most for mutable types like lists, dictionaries, and sets because their contents can change in place.
The key point is that the function parameter is a new name that refers to the existing object. The caller's variable and the function's parameter are two different names for the same underlying list. This is often called "pass by object reference" or "call by sharing."
What "Pass by Object Reference" Means for Lists
To understand the implications, compare Python's behavior with true pass-by-value and pass-by-reference semantics:
- Pass by value: The function receives a copy of the argument. Changes inside the function do not affect the caller.
- Pass by reference: The function receives a reference to the caller's variable itself. Reassigning the parameter inside the function changes the caller's variable.
- Pass by object reference (Python): The function receives a reference to the object. Mutating the object affects the caller, but rebinding the parameter does not.
This distinction is critical. If you reassign the parameter inside the function, you are only changing what the local name points to; the original list remains untouched. But if you call a method that modifies the list in place, the caller sees the change.
Mutating a List Inside a Function
Many list methods modify the list in place. These include append, extend, insert, remove, pop, sort, and reverse. When you call any of these inside a function, the changes are visible to the caller.
def add_item(items, item): items.append(item) cart = ["apple", "banana"] add_item(cart, "orange") print(cart) # ['apple', 'banana', 'orange']
Because items and cart refer to the same list, the append operation modifies the original list. This is often the desired behavior when you want a function to update a collection in place, but it can also lead to unintended side effects if the caller does not expect the list to change.
Reassigning a List Parameter vs Mutating It
A common mistake is to think that assigning a new value to the parameter will update the caller's list. It will not.
def reset_list(items): items = [] # This only rebinds the local name original = [1, 2, 3] reset_list(original) print(original) # [1, 2, 3]
The assignment items = [] creates a new list object and binds it to the local variable items. The original list is still referenced by original and remains unchanged. If you want to clear the list, you must use items.clear() or items[:] = [] to modify the same object.
def clear_list(items): items.clear() original = [1, 2, 3] clear_list(original) print(original) # []
Understanding this distinction prevents subtle bugs where a function appears to do nothing because it only reassigns its parameter.
Avoiding Mutable Default Arguments
One of the most well-known Python pitfalls involves using a list as a default argument. Default arguments are evaluated only once when the function is defined, not on every call. If the default is a mutable object like a list, all calls that rely on the default share the same list.
def add_item_bad(item, items=[]): items.append(item) return items print(add_item_bad(1)) # [1] print(add_item_bad(2)) # [1, 2] # Unexpected!
The second call returns [1, 2] because the default list persists across calls. The standard fix is to use None as the default and create a new list inside the function when the argument is not provided.
def add_item_good(item, items=None): if items is None: items = [] items.append(item) return items print(add_item_good(1)) # [1] print(add_item_good(2)) # [2] # Correct
This pattern ensures that each call without an explicit list gets a fresh list, avoiding shared mutable state across calls.
Returning Lists vs Modifying in Place
When designing a function that processes a list, you have two main options: modify the list in place or return a new list. Each approach has tradeoffs.
Modifying in place is efficient because it avoids creating a new list, but it makes the function's side effects visible to the caller. This is appropriate when the function's purpose is to update a collection, such as sorting or filtering in place. It also matches the behavior of many built-in methods like list.sort().
Returning a new list is safer when you want to keep the original unchanged. This is common in functional programming styles and when the input should remain immutable from the caller's perspective. For example:
def double_values(items): return [x * 2 for x in items] original = [1, 2, 3] result = double_values(original) print(original) # [1, 2, 3] print(result) # [2, 4, 6]
Choose the approach based on the function's contract. If the function name suggests a transformation (like double_values), returning a new list is clearer. If the name suggests an action (like sort), modifying in place is expected.
When to Copy a List Before Passing
If you need to pass a list to a function that might mutate it, but you want to preserve the original, you can pass a copy. Python offers several ways to copy a list:
list.copy(): creates a shallow copy.items[:]: slicing with the full range also creates a shallow copy.copy.deepcopy(items): creates a deep copy for nested lists.
def append_one(items): items.append(1) original = [1, 2, 3] append_one(original.copy()) print(original) # [1, 2, 3]
A shallow copy is sufficient if the list contains only immutable elements or if you only modify the list structure itself. If the list contains nested mutable objects and the function might modify those inner objects, you need a deep copy to fully isolate the original. The copy module provides deepcopy for this purpose.
import copy def add_inner_item(matrix): matrix[0].append(99) original = [[1, 2], [3, 4]] add_inner_item(copy.deepcopy(original)) print(original) # [[1, 2], [3, 4]]
Copying a list before passing it adds overhead, so use it only when you have a specific reason to protect the original. In performance‑sensitive code, consider whether the function can be written to avoid mutation entirely, or document the side effects clearly so callers know to pass a copy if needed.