If you've ever copied a list or dictionary in Python and accidentally modified both, this guide is for you. Copy behavior is one of the most common sources of subtle bugs, especially for beginners. Understanding the difference between shallow copy and deep copy is essential for writing predictable, side-effect-free code.

In this complete guide, you'll learn everything from the basic concept of assignment vs copying to advanced techniques using the copy module from the standard library. We'll explore practical examples with lists, dictionaries, nested objects, and custom classes, along with performance considerations and best practices.

Assignment Is Not Copying

Before diving into copy types, it's crucial to understand that assigning one variable to another does not create a copy. In Python, variables are labels (references) pointing to objects in memory. When you write b = a, you're just creating another label for the same object. The official Python documentation on assignment explains that the = operator binds a name to an object without duplicating it.

# Regular assignment — not a copy
original_list = [1, 2, [3, 4]]
reference_list = original_list

reference_list.append(5) print(original_list) # [1, 2, [3, 4], 5] — original was changed!

This happens because both variables point to the same memory object. Any modification through one reference affects the other. This fundamental concept is what drives the need for explicit copy methods.

What Is a Shallow Copy?

A shallow copy creates a new object, but does not duplicate the inner objects. Instead, it copies the references to the elements contained in the original object. This means the top-level object is new, but nested objects (like lists within lists) are still shared between the copy and the original.

Python offers several ways to create shallow copies. The most common is using the copy module:

import copy

original_list = [1, 2, [3, 4]] shallow_copy = copy.copy(original_list)

shallow_copy.append(5) # only affects the copy shallow_copy[2].append(5) # affects BOTH! Inner object is shared

print(original_list) # [1, 2, [3, 4, 5]] print(shallow_copy) # [1, 2, [3, 4, 5], 5]

Notice that append(5) at the top level only modified the copy, but append(5) on the nested list modified both. That's because the inner list [3, 4] was not duplicated — only the reference to it was copied. The official copy module documentation describes this behavior in detail.

Shallow Copy Methods

Besides copy.copy(), there are other ways to create shallow copies in Python:

  • The .copy() method on lists and dicts: list.copy() and dict.copy() create shallow copies.
  • Slicing ([:]): new_list = original_list[:] creates a shallow copy of the list.
  • The list() and dict() constructors: list(original) and dict(original) also create shallow copies.
  • The copy module: copy.copy(object) works with any object type.

For those working with Python lists, mastering these techniques is essential to avoid unexpected behavior. Real Python has an in-depth tutorial on copying objects in Python that's well worth reading.

What Is a Deep Copy?

A deep copy creates a new object and recursively duplicates every nested object it encounters. The result is a completely independent copy of the original with no shared references. Any modification to the deep copy won't affect the original, and vice versa.

import copy

original_list = [1, 2, [3, 4]] deep_copy = copy.deepcopy(original_list)

deep_copy.append(5) deep_copy[2].append(5)

print(original_list) # [1, 2, [3, 4]] — unchanged! print(deep_copy) # [1, 2, [3, 4, 5], 5]

The copy.deepcopy() function traverses the entire object tree and creates duplicates of every node it finds. This guarantees complete isolation between the copy and the original, but comes at a higher computational cost, especially for large or deeply nested structures. The Stack Overflow discussion on this topic is one of the most upvoted on the platform, showing just how relevant this subject is.

How deepcopy Works Internally

The copy.deepcopy() function uses an internal memo dictionary to track already-copied objects. This is essential for preventing infinite loops in circular reference structures and ensuring shared objects aren't duplicated multiple times. If you're studying Python dictionaries, understanding deep copy is crucial since nested dictionaries are one of the most common use cases.

The mechanism can be broken down into three steps:

  1. Memo check: If the object has already been copied, return the existing copy.
  2. Create new object: Uses __deepcopy__ or __copy__ if defined, or creates a new empty object of the same type.
  3. Recursive copy: For each attribute or element, calls deepcopy recursively and stores it in the new object.

The PEP 8 — Python Style Guide recommends implementing explicit copy methods in classes that require custom copy behavior.

Shallow Copy vs Deep Copy: Side-by-Side Comparison

Let's consolidate the differences into a comparison table for quick reference:

FeatureShallow CopyDeep Copy
Creates new top object?YesYes
Copies nested objects?No (only references)Yes (recursively)
PerformanceFastSlower
Memory usageLowHigh
Full isolation?NoYes
Works with custom objects?Yes (default)Yes (default)
Circular references?No issueYes (via memo)

Copying Different Data Structures

Copying Lists

Lists are the most common structure where the shallow vs deep distinction appears. Here are examples with different nesting levels:

import copy

Flat list (no nesting)

flat = [1, 2, 3] f_shallow = copy.copy(flat) f_deep = copy.deepcopy(flat)

Both work the same here since there are no nested objects

Nested list

nested = [[1, 2], [3, 4]] n_shallow = copy.copy(nested) n_deep = copy.deepcopy(nested)

nested[0].append(99) print(nested) # [[1, 2, 99], [3, 4]] print(n_shallow) # [[1, 2, 99], [3, 4]] — affected! print(n_deep) # [[1, 2], [3, 4]] — isolated

Copying Dictionaries

import copy

data = {"name": "John", "address": {"city": "NY", "zip": "10001"}}

shallow = copy.copy(data) deep = copy.deepcopy(data)

shallow["address"]["city"] = "LA" print(data["address"]["city"]) # LA — shallow copy shares the inner dict

deep["address"]["city"] = "SF" print(data["address"]["city"]) # LA — deep copy kept the original intact

Dictionaries with nested values (like the "address" field above) are the most common scenario where shallow copy causes surprises. Whenever your dictionary contains other dictionaries, lists, or objects as values, consider using deep copy if you need complete isolation.

Copying Custom Objects

For your own classes, copy behavior can be customized by implementing __copy__ and __deepcopy__:

import copy

class Address: def init(self, city, zip_code): self.city = city self.zip_code = zip_code

class Person: def init(self, name, address): self.name = name self.address = address

def __copy__(self):
    # Custom shallow copy
    return Person(self.name, self.address)

def __deepcopy__(self, memo):
    # Custom deep copy
    return Person(
        copy.deepcopy(self.name, memo),
        copy.deepcopy(self.address, memo)
    )

addr = Address("NY", "10001") p1 = Person("John", addr) p2 = copy.deepcopy(p1)

p2.address.city = "LA" print(p1.address.city) # NY — isolated thanks to deepcopy

The __deepcopy__ method documentation explains that the memo parameter is a dictionary tracking already-copied objects to prevent duplication and infinite loops.

When to Use Each Copy Type

Choosing between shallow and deep copy depends on your use case. Here are practical guidelines:

Prefer Shallow Copy When:

  • The contained objects are immutable (ints, strings, tuples without mutable objects).
  • You want efficiency and the structure has only one level of depth.
  • You intentionally want to share inner objects (e.g., a shared cache).
  • Working with large datasets where deep copy would be prohibitively expensive.

Prefer Deep Copy When:

  • You have mutable nested objects at multiple levels.
  • You need complete isolation between original and copy.
  • Implementing patterns like state snapshots or rollback.
  • You don't control who might modify the inner objects.

Performance and Best Practices

Deep copy is significantly more expensive than shallow copy, both in execution time and memory usage. For large objects, the difference can span orders of magnitude. If you're processing gigabytes of data, a deep copy can easily exhaust available memory.

Best practices for working with copies in Python:

  1. Document the behavior: If your class implements __copy__ or __deepcopy__, document what gets copied shallowly or deeply.
  2. Prefer immutability: Use tuples, frozensets, and immutable types whenever possible. They never need deep copying.
  3. Consider alternatives: Instead of copying, sometimes it's better to design your code to not need copies (e.g., using factory functions).
  4. Be careful with deepcopy on complex objects: Objects with network connections, open files, or locks should not be deep-copied.

The official deepcopy documentation warns that objects like modules, classes, functions, file handles, and generators cannot be copied. The pickle module is an alternative for serialization and copying in some scenarios.

Advanced Cases

Circular References

Structures with circular references (an object containing a reference to itself) are handled correctly by deepcopy thanks to the memo parameter:

import copy

class Node: def init(self, value): self.value = value self.next = None

a = Node(1) b = Node(2) a.next = b b.next = a # circular reference!

copy_node = copy.deepcopy(a) # works without issues print(copy_node.next.next.value) # 1

Copying with Filters

You can control which attributes get copied by implementing __deepcopy__ selectively:

import copy

class Config: def init(self): self.data = {"key": "value"} self.cache = {"temp": "data"} # we don't want to copy this

def __deepcopy__(self, memo):
    new = Config()
    new.data = copy.deepcopy(self.data, memo)
    # cache is not copied — it will be recreated empty
    return new

Common Mistakes and How to Avoid Them

Here are the most frequent copy-related errors in Python and how to avoid them:

Mistake #1: Assuming = Creates a Copy

As we saw at the start, b = a creates a new reference, not a copy. Always use copy() or deepcopy() when you need independence.

Mistake #2: Using copy.copy() on Deeply Nested Structures

If your structure has mutable objects at multiple levels, shallow copy won't suffice. Use deepcopy() or reconsider the design.

Mistake #3: Applying deepcopy Indiscriminately

Deep copy is expensive. Use it only when necessary. For simple or immutable objects, shallow copy is sufficient and far more efficient. The GeeksForGeeks article on Python copy shows practical examples of this performance difference.

Mistake #4: Ignoring Non-Copyable Objects

Not all objects can be deep-copied. Database connections, sockets, threads, and OS-level objects will fail. Always check the documentation of the library you're using. The official Python data structures tutorial is an excellent starting point for understanding these concepts in depth.

Conclusion

Mastering the difference between copy and deep copy in Python is an essential skill for any developer working with mutable data structures. Choosing correctly between shallow and deep copy can prevent hard-to-find bugs and significantly improve the predictability of your code.

The golden rule is simple: use shallow copy for flat objects or when you know the inner objects are immutable; use deep copy when there are mutable nested objects and you need complete isolation. And above all, remember that assignment (=) is never a copy.

Keep exploring the Python Universe with our free guides on Python lists and Python dictionaries to deepen your data manipulation skills.