Python Shallow Reference Copy: What Gets Copied
python shallow reference copy: Understand how Python's shallow copy differs from reference assignment and deep copy, and when each approach is appropriate in real code.
When you write b = a in Python, you are not copying anything. You are binding a second name to the same object. This is the reference behavior that makes the phrase "python shallow reference copy" necessary: there is a real distinction between rebinding a name, creating a shallow copy, and creating a deep copy. Understanding that distinction prevents a class of bugs where mutating one variable silently changes another.
What Assignment Really Does in Python
Python variables are names bound to objects. Assignment does not duplicate the object; it makes the name on the left refer to the same object the name on the right refers to.
original = [1, 2, 3] alias = original alias.append(4) print(original) # [1, 2, 3, 4] print(alias) # [1, 2, 3, 4]
Both names point to the same list object. The append call mutates that single object, so both names observe the change. This is not a copy of any kind; it is a reference assignment.
The same behavior applies to dictionaries, sets, and any custom class instance. Only immutable objects such as integers, strings, and tuples are safe from this effect, because their values cannot be changed in place. Rebinding a name to a new immutable value never affects other names pointing at the old value.
Shallow Copy with copy.copy()
The copy module provides copy.copy(), which creates a new top-level object while sharing nested objects with the original.
import copy original = [1, 2, [3, 4]] shallow = copy.copy(original) shallow.append(5) print(original) # [1, 2, [3, 4]] print(shallow) # [1, 2, [3, 4], 5]
The outer list is a new object, so appending to shallow does not affect original. But the nested list [3, 4] is shared:
shallow[2].append(99) print(original) # [1, 2, [3, 4, 99]] print(shallow) # [1, 2, [3, 4, 99], 5]
This is the defining characteristic of a shallow copy: one level of structure is duplicated, and everything below that level remains shared.
For built-in containers, copy.copy() also works on dictionaries and sets:
data = {"users": ["alice"], "count": 1} shallow = copy.copy(data) shallow["count"] = 2 # rebinds the key, original unaffected shallow["users"].append("bob") # mutates the shared list print(data) # {'users': ['alice', 'bob'], 'count': 1} print(shallow) # {'users': ['alice', 'bob'], 'count': 2}
Rebinding a key in the shallow copy does not touch the original dictionary. Mutating a value that is itself a mutable object does.
Where Shallow Copy Stops: Nested Objects
The boundary of a shallow copy is exactly one level deep. Any mutable object stored inside the copied container is shared between the original and the copy. This matters most with:
- lists of lists
- dictionaries whose values are lists or dicts
- objects that hold references to other mutable objects
matrix = [[1, 2], [3, 4]] shallow = copy.copy(matrix) shallow[0][0] = 99 print(matrix) # [[99, 2], [3, 4]] print(shallow) # [[99, 2], [3, 4]]
The inner lists were never duplicated. Modifying an element of an inner list changes the shared inner list, and both outer containers observe the change.
This is not a bug in copy.copy(). It is the documented contract. A shallow copy preserves sharing below the top level, which is sometimes exactly what you want, and sometimes the source of a subtle bug.
Deep Copy as the Alternative
When the nested structure must be fully independent, copy.deepcopy() recursively duplicates every object in the graph.
import copy matrix = [[1, 2], [3, 4]] deep = copy.deepcopy(matrix) deep[0][0] = 99 print(matrix) # [[1, 2], [3, 4]] print(deep) # [[99, 2], [3, 4]]
Deep copy walks the entire object graph, creating new objects at every level. The result shares nothing mutable with the original.
Deep copy has real costs. It is slower because it traverses every reachable object, and it can fail on objects that cannot be pickled or that contain non-copyable resources such as open file handles, database connections, or locks. The copy module handles many built-in types and user classes that define __copy__ or __deepcopy__, but objects with external state often need custom handling.
Performance and Memory Tradeoffs
Shallow copy is cheap: it allocates one new container and copies references into it. The cost is proportional to the number of items at the top level, not the total size of the nested structure.
Deep copy is expensive: it must visit every reachable object, allocate new containers at every level, and maintain a memo dictionary to handle cycles and repeated references. For a large nested structure, deep copy performs far more allocation and traversal work than shallow copy.
The practical implication is that deep copy should be used only when the nested structure genuinely needs to be independent. When nested objects are read-only or intentionally shared, a shallow copy avoids that cost entirely.
Choosing the Right Copying Strategy
Use plain assignment when you want two names to refer to the same object and mutations should be visible through both.
Use copy.copy() when you need a new top-level container but the nested objects should remain shared. This is common when duplicating a configuration dict whose leaf values are immutable, or when copying a list of objects that are expensive to duplicate and are not mutated in place.
Use copy.deepcopy() when the entire object graph must be independent, such as when you are about to mutate nested structures and the original must remain untouched, or when passing an object to code that may modify it and you need to keep your own version intact.
A practical rule: default to shallow copy unless you can demonstrate that nested mutation will cross the copy boundary. Deep copy is the safer choice for correctness, but the extra cost and the risk of failing on non-copyable objects mean it should be applied deliberately rather than habitually.
Handling Custom Classes and copy
Classes can control how copy.copy() behaves by defining __copy__. This is useful when a default shallow copy would copy the wrong thing, such as a large internal cache that should be shared or a resource that should not be duplicated.
class Buffer: def __init__(self, data): self.data = data self.cache = {} def __copy__(self): new = type(self)(self.data) new.cache = self.cache # share the cache deliberately return new
Without __copy__, copy.copy() creates a new instance and copies the instance dictionary, which shares the cache dict anyway. The explicit method makes the sharing intentional and documents the behavior for other developers.
For __deepcopy__, the method receives a memo dictionary and must use it to handle references correctly, especially when the object graph contains cycles. A correct implementation looks like:
def __deepcopy__(self, memo): new = type(self)(copy.deepcopy(self.data, memo)) memo[id(self)] = new return new
The memo argument prevents infinite recursion when two objects reference each other. Without it, a cyclic structure would cause deepcopy to recurse until the recursion limit is reached.