Back to Blog
Python

Python List Mutability: Assignment and Function Calls

python list mutability: Learn how Python list mutability affects assignment, function calls, and copying, and avoid common pitfalls like shared references and mutable...

list mutabilitypython referencesfunction argumentscopying listsmutable default arguments
Illustration of Python list mutability showing two variables referencing the same list and one mutation affecting both.

In Python, lists are mutable objects: you can change their contents in place without creating a new list. This behavior is convenient, but it also means that the same list object can be referenced from multiple places, and changes made through one reference are visible through all others. Understanding python list mutability is essential for writing predictable code, especially when passing lists to functions or storing them in data structures.

What Does Mutability Mean for a List?

A mutable object can be modified after it is created. For lists, this includes changing an element by index, appending, removing, extending, and sorting in place. Consider this simple example:

nums = [1, 2, 3] nums.append(4) nums[0] = 0 print(nums) # [0, 2, 3, 4]

The list object nums is changed directly; no new list is created. This is different from immutable types like strings and tuples, where any operation that appears to modify the object actually returns a new object.

How Assignment Creates References, Not Copies

When you assign a list to another variable, you are not creating a copy. Both variables refer to the same list object. This is often called aliasing. For example:

a = [1, 2, 3] b = a b.append(4) print(a) # [1, 2, 3, 4]

Because a and b point to the same list, the mutation through b is visible through a. To create an independent copy, use the copy() method, the list() constructor, or slicing:

a = [1, 2, 3] b = a.copy() b.append(4) print(a) # [1, 2, 3] print(b) # [1, 2, 3, 4]

These methods produce a shallow copy: the list container is new, but the elements are still shared. For lists of immutable objects (like integers or strings), this is usually sufficient.

Passing Lists to Functions: The Caller Is Not Safe

When you pass a list to a function, the function receives a reference to the same object. If the function mutates the list, the caller sees the change. This can be intentional, but it often leads to bugs when you expect the function to leave the input unchanged.

def add_item(lst, item): lst.append(item) my_list = [1, 2] add_item(my_list, 3) print(my_list) # [1, 2, 3]

To avoid modifying the original, you can pass a copy or have the function return a new list. The choice depends on the design. If the function is meant to transform data, returning a new list is often clearer:

def add_item_copy(lst, item): new_lst = lst.copy() new_lst.append(item) return new_lst

Avoiding Unintended Mutation: Copying Lists

As mentioned, shallow copies share the same elements. For nested lists, this can cause surprising behavior. Consider:

original = [[1, 2], [3, 4]] shallow = original.copy() shallow[0].append(99) print(original) # [[1, 2, 99], [3, 4]]

The inner list is shared between original and shallow. To create a fully independent copy, use copy.deepcopy():

import copy original = [[1, 2], [3, 4]] deep = copy.deepcopy(original) deep[0].append(99) print(original) # [[1, 2], [3, 4]]

Deep copying recursively creates new objects for all nested elements. It is more expensive than a shallow copy, so use it only when necessary.

Mutability and Memory: The Tradeoff

Mutating a list in place avoids allocating a new list, which can be more efficient when working with large collections. For example, repeatedly appending to a list is generally cheaper than concatenating lists, which creates a new list each time. However, aliasing can lead to unintended memory sharing. If you copy a list, you create a new container, but the elements are still shared unless you deep copy. This means that modifying an element in one list can affect another if the element itself is mutable.

Consider the performance implication: a shallow copy of a list with a million integers is cheap because it only copies references. A deep copy would be much more expensive because it creates new integer objects (though integers are immutable, so deep copy may not be needed). In practice, you should choose the copying strategy based on whether the elements are mutable and whether you need independent copies.

Common Pitfalls: Default Arguments and Class Attributes

A classic mistake is using a mutable default argument in a function definition:

def add_item(item, lst=[]): lst.append(item) return lst

The default list is created once when the function is defined and shared across all calls. This leads to unexpected accumulation:

print(add_item(1)) # [1] print(add_item(2)) # [1, 2]

The correct pattern is to use None and create a new list inside the function:

def add_item(item, lst=None): if lst is None: lst = [] lst.append(item) return lst

Similarly, class attributes that are lists are shared across instances. If you define a list at the class level and mutate it through an instance, all instances see the change. Use instance attributes instead:

class MyClass: def __init__(self): self.items = [] # instance attribute

When to Choose Immutable Alternatives

Sometimes you want to guarantee that a sequence cannot be modified. Tuples are immutable and can be used where a fixed collection is required. They also can be used as dictionary keys or set elements, which lists cannot. The table below summarizes the key differences:

FeatureListTuple
MutabilityMutableImmutable
Methods for modificationappend, remove, etc.None
HashableNoYes (if elements are hashable)
Typical useDynamic collectionsFixed records, function arguments

If you need to pass a collection to a function and want to prevent accidental modification, consider using a tuple. However, if you need to modify the collection, a list is appropriate. The choice depends on whether the data is expected to change.

python list mutability: Practical Usage and Code Examples | RYUSLOG DEV