Back to Blog
Python

Python List Copy vs Deepcopy: Key Differences

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

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 distinction between python list copy vs deepcopy comes down to how nested references are handled. A shallow copy duplicates the outer list but shares the inner objects, while 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 works on dictionaries, sets, and any object that can be pickled or has a defined __deepcopy__ method. It also handles cycles correctly. If a list contains a reference to itself, deepcopy will not recurse infinitely; it keeps a memo of already copied objects.

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, the module attempts to reconstruct the object by creating a new instance and copying its __dict__ or __slots__.

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 significantly more expensive than a shallow copy because it traverses the entire object graph. Every nested list, dictionary, or custom object is duplicated, which increases both CPU time and memory usage. For a flat list of immutable values, a shallow copy is nearly free. For a deeply nested structure, deepcopy can be orders of magnitude slower.

There is no built-in benchmark to quote, but the mechanism is clear: shallow copy allocates one new list and copies references. 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 only safe option, and the cost is unavoidable.

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 (ints, strings, tuples, frozensets).
  • 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 has cycles or shared references that must be preserved independently.

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 handle 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.

python list copy vs deepcopy: Practical Usage and Code Examp | RYUSLOG DEV