Back to Blog
Python

Python Shallow Copy vs Deep Copy: Key Differences

python shallow copy vs deep copy: Understand the difference between shallow and deep copy in Python, when nested objects are shared, and how to choose the right copyin...

Pythoncopy moduledeepcopymutable objectsmemory management
Illustration of shallow and deep copy in Python showing shared nested lists versus duplicated nested lists.

When you assign one Python object to a second variable, you do not get a copy. You get another name for the same object. That is harmless for immutable values, but it becomes a real problem as soon as the object is mutable. The copy module provides two ways to actually duplicate an object: copy.copy() for a shallow copy and copy.deepcopy() for a deep copy. The difference between a python shallow copy vs deep copy decides whether nested mutable objects are shared between the original and the copy, or duplicated independently.

Assignment Does Not Create a Copy

original = [[1, 2], [3, 4]] alias = original alias[0][0] = 99 print(original) # [[99, 2], [3, 4]]

alias is not a copy at all. Both names reference the same list object, so any mutation through one name is visible through the other. This is the baseline problem that copying functions are meant to solve.

Shallow Copy: New Container, Shared Contents

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

A shallow copy creates a new outer container but fills it with references to the same inner objects. The outer list is a distinct object, but the two inner lists are shared. Mutating a nested element therefore changes the original.

The same is true for the copy methods on built-in containers:

a = {"items": [1, 2, 3]} b = a.copy() b["items"].append(4) print(a["items"]) # [1, 2, 3, 4]

dict.copy(), list.copy(), and the slice syntax [:] are all shallow. They protect the top level only.

Deep Copy: Recursively Duplicated Objects

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

deepcopy walks the entire object graph and creates new objects for every mutable container it encounters. The result is fully independent of the original. Nested lists, dictionaries, sets, and custom objects are all duplicated.

This is the behavior to reach for when the copy must be modified without any effect on the source object.

How deepcopy Handles Circular References

A naive recursive copy would loop forever on a self-referential structure. deepcopy avoids this by keeping a memo dictionary that maps already-copied objects to their copies.

import copy node = [] node.append(node) clone = copy.deepcopy(node) print(clone[0] is clone) # True

The clone preserves the circular structure: the inner element of the clone is the clone itself. Without the memo, this example would recurse until the interpreter raised a recursion error.

The memo also guarantees that an object referenced from two places in the source graph is copied only once, so shared references inside the original remain shared inside the copy.

Performance and Memory Tradeoffs

A shallow copy allocates one new container and copies the references from the source. Its cost is proportional to the number of top-level entries, and the nested objects are not touched.

A deep copy traverses every reachable object and allocates a new object for each mutable container. It also maintains the memo dictionary during the traversal. For a large or deeply nested structure, this is noticeably more expensive in both time and memory.

There is no reason to pay that cost when the nested objects will never be mutated. If the inner data is effectively read-only, a shallow copy is sufficient and avoids duplicating large subtrees. If any nested object can be modified later, a shallow copy leaves the two structures coupled, and a deep copy is the safer choice.

Custom Objects: copy and deepcopy

For your own classes, the default behavior of copy.copy and copy.deepcopy is usually acceptable: copy.copy creates a new instance and copies __dict__, while copy.deepcopy recursively copies every attribute. When that default is wasteful or wrong, you can implement the two hooks.

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

__copy__ controls shallow copying and __deepcopy__ controls deep copying. The memo argument must be passed through when you recurse into attributes, or circular references inside the object will break the same way they would without a memo.

A common reason to define these hooks is to share an expensive, immutable resource between copies while still duplicating the mutable parts.

When to Choose Shallow vs Deep

The decision comes down to whether any nested object will be mutated after the copy is made.

Use a shallow copy when:

  • the container itself needs to change (adding or removing top-level keys or elements)
  • the nested objects are treated as immutable
  • you explicitly want the copy to observe later mutations of the shared inner data

Use a deep copy when:

  • nested objects will be modified independently
  • the original must remain unchanged no matter what happens to the copy
  • you are storing or passing a snapshot of a mutable structure

For a flat structure containing only immutable values, both approaches produce equivalent results, and a shallow copy is cheaper.

Edge Cases: Tuples and Immutable Containers

Immutability does not make a shallow copy unnecessary. A tuple can contain a list, and that list remains mutable.

import copy t = (1, [2, 3]) s = copy.copy(t) s[1].append(4) print(t) # (1, [2, 3, 4])

The tuple itself cannot be reassigned, but the list inside it can be changed. A shallow copy of the tuple still shares that list, so the original tuple appears to change. A deep copy duplicates the list:

d = copy.deepcopy(t) d[1].append(5) print(t) # (1, [2, 3, 4]) print(d) # (1, [2, 3, 4, 5])

copy.copy on a tuple of only immutable values returns the same tuple object, since there is nothing to protect. The same logic applies to frozenset and other immutable containers: copying is only meaningful when the object graph contains at least one mutable element.

python shallow copy vs deep copy: Practical Usage and Code E | RYUSLOG DEV