Back to Blog
Python

Python List Copy vs Deepcopy: Key Differences

Understand the difference between shallow and deep copies of Python lists, when to use copy() vs deepcopy(), and how nested structures behave.

list copydeepcopyshallow copynested listscopy module
Diagram showing a shallow copy sharing nested list references versus a deep copy creating independent nested lists.

When you copy a Python list, the behavior depends on whether the list contains other mutable objects. The real distinction between copy() and deepcopy() comes down to how nested references are handled. A shallow copy duplicates the outer list but shares the inner objects; a deep copy recursively duplicates everything.

What a Shallow Copy Actually Duplicates

Assigning a list to a new variable does not create a copy at all. Both names reference the same list object, so any mutation through one name is visible through the other.

original = [[1, 2], [3, 4]] assigned = original assigned.append(5) print(original) # [[1, 2], [3, 4], 5]

The copy() method and the list() constructor produce a shallow copy. The outer list is new, but the elements inside are the same objects. For a list of integers, this is usually fine because integers are immutable. For a list that contains other lists or dictionaries, the inner objects are shared.

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

Both lists now show the change because shallow[0] and original[0] point to the same inner list. This is the core behavior that separates a shallow copy from a deep copy.

When You Need a Deep Copy: Nested Lists and Mutable Objects

The copy.deepcopy() function from the copy module recursively copies every object it encounters. The result is a fully independent structure where no references are shared with the original.

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

Here, modifying deep[0] does not affect original[0]. This is the behavior you want when a list contains mutable objects and you need to treat the copy as an independent snapshot.

deepcopy is not limited to lists. It can also copy dictionaries, sets, many custom objects, and cyclic structures. If a list contains a reference to itself, deepcopy keeps a memo of already copied objects, so it does not recurse infinitely.

How the copy Module Handles Custom Objects and Cycles

For custom classes, deepcopy checks for a __deepcopy__ method. If the class defines it, that method controls how the object is copied. Otherwise, it falls back to its default reconstruction logic, which typically creates a new instance and copies the object's state.

Cycles are a common reason to prefer deepcopy over manual recursion. Consider a list that contains itself:

import copy lst = [] lst.append(lst) deep = copy.deepcopy(lst) print(deep[0] is deep) # True

The deep copy preserves the self-referential structure without hitting a recursion limit. A naive recursive copy would fail.

Performance and Memory Cost of Deepcopy

deepcopy is more expensive than a shallow copy because it traverses the entire object graph. Every nested list, dictionary, or custom object is duplicated, increasing both CPU time and memory use. For a flat list of immutable values, a shallow copy is cheap. For a deeply nested structure, deepcopy can be substantially slower.

The mechanism explains the cost: a shallow copy allocates one new list and copies references, while deepcopy allocates a new list and recursively allocates new objects for every mutable element. If you are copying a large list of integers, copy() is the right choice. If you need independence for nested structures, deepcopy is the safe option, and the extra cost is the price of correctness.

Choosing Between copy() and deepcopy() Based on Your Data

The decision rests on what the list contains and what you plan to do with the copy.

Use copy() or slicing when:

  • The list contains only immutable objects, such as ints, strings, frozensets, or tuples that contain only immutable objects.
  • You need a new outer list but are comfortable sharing the inner objects.
  • The list is flat and you only add or remove top-level elements.

Use deepcopy() when:

  • The list contains nested lists, dictionaries, or custom mutable objects.
  • You need to modify the copy without affecting the original.
  • The structure contains cycles or shared references that need to be handled correctly.

A common mistake is to use copy() on a list of lists and then assume the inner lists are independent. This leads to subtle bugs where changes appear in both structures. If you are unsure whether the elements are mutable, inspect the data or use deepcopy when in doubt.

Common Pitfalls: Shared References and Unexpected Mutations

Shallow copies are the source of many hard-to-find bugs. The most frequent mistake is copying a list of dictionaries and then updating a dictionary in the copy.

records = [{"id": 1}, {"id": 2}] copy_records = records.copy() copy_records[0]["id"] = 99 print(records) # [{'id': 99}, {'id': 2}]

The original list changes because the dictionary object is shared. The same applies to nested lists. To avoid this, use deepcopy whenever the elements are mutable.

Another pitfall is relying on slicing [:] to create a deep copy. Slicing is just a shallow copy. The syntax is convenient, but it does not change the semantics.

Alternative Copy Approaches and Their Limits

The list() constructor and slicing both produce shallow copies. You can also use copy.copy() for a generic shallow copy of any object. None of these recursively copy nested structures.

For a custom deep-copy behavior, you can implement __deepcopy__ on your class. This is useful when the default deepcopy logic is too broad or when you want to reuse certain sub-objects intentionally.

import copy class Config: def __init__(self, values): self.values = values def __deepcopy__(self, memo): return Config(copy.deepcopy(self.values, memo))

This method receives the memo dictionary used by deepcopy to track already copied objects. Passing it to nested calls prevents duplicate copies and handles cycles correctly.

When performance matters and you are copying large nested structures frequently, consider whether you can avoid the copy altogether. Sometimes you can restructure the code to share read-only data or use immutable alternatives like tuples. But if you need a true independent copy, deepcopy is the standard tool, and its cost is the price of correctness.