Python List Copy vs Slicing: What Actually Happens
python list copy vs slicing: Understand how Python list copy() and slicing differ, when shallow copies share nested data, and how to choose the right approach.
When you write new_list = old_list[:] or new_list = old_list.copy(), both create a new list object. The key question behind python list copy vs slicing is whether these operations behave identically in every situation. The short answer is that they are functionally equivalent for most cases, but the distinction matters when you look at nested structures, memory behavior, and code that must run on older interpreters.
What Slicing Actually Returns
Slicing with [:] returns a new list containing references to the same elements. The slice operator allocates a new list object and copies the references from the original list into it. This is why old_list[:] is old_list evaluates to False while old_list[:] == old_list evaluates to True.
original = [1, 2, 3] sliced = original[:] print(sliced is original) # False print(sliced == original) # True
The is comparison checks object identity, while == checks value equality. Both copy() and slicing produce a distinct list object with the same contents. The identity difference is the entire point of copying: you get a separate object you can mutate without affecting the original.
Shallow Copy Semantics
Both copy() and slicing perform a shallow copy. The new list holds references to the same objects as the original list. For immutable elements like integers and strings, this distinction rarely matters because those objects cannot be modified in place.
numbers = [1, 2, 3] copy_one = numbers.copy() copy_two = numbers[:] copy_one.append(4) print(numbers) # [1, 2, 3] print(copy_one) # [1, 2, 3, 4]
Appending to the copy does not affect the original because the list object itself is distinct. The confusion arises when the list contains mutable objects, because those objects are shared between the original and the copy.
Nested Lists and the Shallow Copy Trap
Consider a list that contains another list:
matrix = [[1, 2], [3, 4]] shallow = matrix[:] shallow[0].append(99) print(matrix) # [[1, 2, 99], [3, 4]]
The inner list [1, 2] is shared between matrix and shallow. Modifying it through either reference changes the data visible through both. This is not a bug in slicing or copy(); it is the defined behavior of shallow copy. If you need independent nested lists, you must use copy.deepcopy().
import copy matrix = [[1, 2], [3, 4]] deep = copy.deepcopy(matrix) deep[0].append(99) print(matrix) # [[1, 2], [3, 4]] print(deep) # [[1, 2, 99], [3, 4]]
deepcopy recursively copies every nested object, guaranteeing full independence at the cost of additional time and memory.
Performance and Memory Characteristics
Slicing and copy() have the same runtime complexity: O(n), where n is the number of elements in the list. Both allocate a new list and copy n references. There is no meaningful performance difference between the two for typical workloads.
The more important performance consideration is the difference between shallow and deep copying. A shallow copy of a large list is fast because it only copies references. A deep copy must traverse the entire object graph, which can be substantially slower for deeply nested structures.
The cost difference comes from the number of objects created. A shallow copy creates one new list. A deep copy creates a new list plus a new object for every mutable element in the original structure. For a list of 10,000 inner lists, a deep copy creates 10,001 new objects, while a shallow copy creates only one. This allocation overhead is the dominant factor, not the copy operation itself.
Choosing Between copy(), Slicing, and Alternatives
The decision between copy() and slicing is largely stylistic. Both are shallow copies with identical behavior. Python's copy() method was introduced in Python 3.3, so slicing is the more backward-compatible choice for code that must run on older interpreters.
The list() constructor is a third option that also performs a shallow copy:
original = [1, 2, 3] constructed = list(original)
All three approaches produce a new list with the same elements. The list() constructor is sometimes preferred when the source is not guaranteed to be a list, since it accepts any iterable. For nested structures, the choice is not between copy() and slicing but between shallow and deep copying. Use copy.deepcopy() only when you need full independence of nested objects, and be aware of the cost.
When Slicing Is Not a Full Copy
Slicing with a start and end index does not always copy the entire list. A slice like data[1:3] returns a new list containing only the elements at indices 1 and 2. This is still a new list object, but it contains a subset of the references.
data = [10, 20, 30, 40] subset = data[1:3] print(subset) # [20, 30]
The full slice [:] is a special case that copies every element. Understanding this distinction helps avoid confusion when reading code that uses partial slices for extraction rather than duplication.
A Note on Memory Sharing with Slices
A common misconception is that slicing creates a view into the original list, similar to how NumPy arrays behave. Python lists do not support views. Every slice creates a new list object with copied references. If you need view-like behavior, you must use a different data structure, such as an array from the array module or a NumPy array, where slicing returns a view by default.
import array arr = array.array('i', [1, 2, 3, 4]) view = arr[1:3] view[0] = 99 print(arr) # array('i', [1, 99, 3, 4])
This behavior is specific to array and NumPy, not to built-in Python lists. Knowing this difference prevents incorrect assumptions when moving between list and array code, and it clarifies why modifying a slice of a list never affects the source list.