Python
Lists
Value vs Reference
Data Structures
Programming Tips

python list by value not by reference

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

In Python, assigning a list to a new variable does not create a copy — it creates a second reference to the same list object. Modifying the list through either variable affects both. To get an independent copy, you need to explicitly create one using slicing (list[:]), list.copy(), list(), or copy.deepcopy() for nested structures. Understanding this behavior is essential for avoiding subtle bugs where one function unintentionally modifies another's data.

The Reference Problem

python
1original = [1, 2, 3]
2alias = original  # Both variables point to the SAME list
3
4alias.append(4)
5print(original)  # [1, 2, 3, 4] — original is modified!
6
7print(original is alias)  # True — same object in memory
8print(id(original) == id(alias))  # True

alias = original does not copy the list. Both names refer to the same object. Any mutation through one variable is visible through the other.

Shallow Copy Methods

python
1original = [1, 2, 3]
2
3# Method 1: Slice
4copy1 = original[:]
5
6# Method 2: list() constructor
7copy2 = list(original)
8
9# Method 3: .copy() method
10copy3 = original.copy()
11
12# Method 4: copy module
13import copy
14copy4 = copy.copy(original)
15
16# All create independent copies
17copy1.append(4)
18print(original)  # [1, 2, 3] — unchanged
19print(copy1)     # [1, 2, 3, 4]
20
21# Verify they are different objects
22print(original is copy1)  # False

All four methods create a new list object with the same elements. Modifying the copy does not affect the original. These are called "shallow copies" because they copy the list structure but not the objects inside it.

The Shallow Copy Trap

python
1original = [[1, 2], [3, 4], [5, 6]]
2shallow = original.copy()
3
4# The outer list is independent
5shallow.append([7, 8])
6print(len(original))  # 3 — not affected
7
8# But the inner lists are SHARED
9shallow[0].append(99)
10print(original[0])  # [1, 2, 99] — original is modified!
11
12print(original[0] is shallow[0])  # True — same inner list

A shallow copy creates a new outer list, but the elements inside still reference the same objects. For immutable elements (ints, strings, tuples), this is fine. For mutable elements (lists, dicts), changes propagate between the copy and the original.

Deep Copy for Nested Structures

python
1import copy
2
3original = [[1, 2], [3, 4], {'key': 'value'}]
4deep = copy.deepcopy(original)
5
6# Modify the deep copy
7deep[0].append(99)
8deep[2]['key'] = 'changed'
9
10print(original)  # [[1, 2], [3, 4], {'key': 'value'}] — unchanged
11print(deep)      # [[1, 2, 99], [3, 4], {'key': 'changed'}]
12
13# All nested objects are independent
14print(original[0] is deep[0])  # False
15print(original[2] is deep[2])  # False

copy.deepcopy() recursively copies every nested object, creating a fully independent clone. Use it when your list contains mutable elements that should not be shared.

Function Arguments

python
1def add_item(items, new_item):
2    items.append(new_item)  # Modifies the original list!
3    return items
4
5my_list = [1, 2, 3]
6add_item(my_list, 4)
7print(my_list)  # [1, 2, 3, 4] — modified by the function
8
9# To avoid modifying the original, copy inside the function
10def add_item_safe(items, new_item):
11    result = items.copy()
12    result.append(new_item)
13    return result
14
15my_list = [1, 2, 3]
16result = add_item_safe(my_list, 4)
17print(my_list)  # [1, 2, 3] — unchanged
18print(result)   # [1, 2, 3, 4]

Python passes objects by reference. When a function receives a list, it gets a reference to the same list. Mutations inside the function affect the caller's data. Copy the list if the function should not modify the original.

Default Mutable Arguments

python
1# BUG: Mutable default argument is shared across calls
2def append_to(item, target=[]):
3    target.append(item)
4    return target
5
6print(append_to(1))  # [1]
7print(append_to(2))  # [1, 2] — unexpected!
8print(append_to(3))  # [1, 2, 3] — keeps growing
9
10# FIX: Use None as default
11def append_to_fixed(item, target=None):
12    if target is None:
13        target = []
14    target.append(item)
15    return target
16
17print(append_to_fixed(1))  # [1]
18print(append_to_fixed(2))  # [2] — fresh list each time

Default mutable arguments are created once when the function is defined, not each time it is called. This is one of the most common Python gotchas related to reference semantics.

List Comprehension Creates a New List

python
1original = [1, 2, 3, 4, 5]
2
3# Comprehension always creates a new list
4filtered = [x for x in original if x > 2]
5doubled = [x * 2 for x in original]
6
7original.append(6)
8print(filtered)  # [3, 4, 5] — not affected
9print(doubled)   # [2, 4, 6, 8, 10] — not affected

List comprehensions and generator expressions that materialize into lists (list(gen)) always produce new list objects. They are a natural way to create independent copies with optional transformation.

When to Use Each Copy Method

ScenarioMethodCopy Depth
Flat list of immutableslist.copy() or [:]Shallow (sufficient)
Flat list of mutablescopy.deepcopy()Deep
Nested lists/dictscopy.deepcopy()Deep
Filtering or transformingList comprehensionNew list
Function argument protectionlist.copy() at call siteShallow

Common Pitfalls

  • Assuming = copies a list: new = old creates an alias, not a copy. Any change to new changes old. Always use .copy(), [:], or list() to copy.
  • Using shallow copy for nested lists: .copy() only copies the top-level list. Nested mutable objects are still shared. Use copy.deepcopy() for nested structures.
  • Mutable default arguments: def f(items=[]) shares the same list across all calls. Use None as the default and create a new list inside the function.
  • Forgetting that slicing creates copies but indexing does not: original[:] creates a copy, but original[0] returns a reference to the element. For mutable elements like original[0] being a list, modifying it changes the original.
  • Overusing deepcopy: copy.deepcopy() is slow because it recursively copies every object and handles circular references. For flat lists of simple types, .copy() is sufficient and much faster.

Summary

  • new = old creates a reference (alias), not a copy — both variables point to the same list
  • Use .copy(), [:], list(), or copy.copy() for shallow copies of flat lists
  • Use copy.deepcopy() when the list contains nested mutable objects (lists, dicts)
  • Functions receive list references — copy before mutating if the original should be preserved
  • Avoid mutable default arguments (def f(items=[])) — use None and create a new list inside
  • List comprehensions always produce new independent lists

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.