Python Copying and Aliasing: Know Which Object Your Code Changes
Trace shared references, nested mutations, and deepcopy memoization with executable examples before choosing a copying policy.
Reviewed October 8, 2026. Examples target Python 3.12 and use synthetic records.
An experiment becomes difficult to reproduce when its preparation silently changes the input for the next experiment. In Python, this can happen without any unusual syntax: a second variable points to a list, a dictionary contains another mutable dictionary, or a function modifies an object supplied by its caller. The important question is not whether two values look equal. It is whether they refer to the same object, and which parts of that object may change.
This article develops a copying policy for small data workflows. You will distinguish assignment from copying, trace mutations through nested containers, and see why a deep copy can preserve shared references inside its result. The examples intentionally avoid performance claims. The appropriate choice depends on object structure, ownership, and the changes your program needs to make.
1. Start with bindings rather than boxes of values
Assignment binds a name to an object. If second = first, both names refer to the same object; Python does not create a duplicate merely because there are now two names. This is harmless when the program only reads the object. It becomes observable when either name is used to mutate a shared list or dictionary.
Rebinding and mutation are different operations. Assigning a new list to second changes what that name refers to. Appending to the existing list changes the object itself, which remains visible through first. This distinction also explains function calls: a parameter gives the function another reference to the supplied object. Reassigning the parameter does not reassign the caller's variable, but mutating the referenced object can affect the caller.
Equality, tested with ==, answers whether values compare equal. Identity, tested with is, answers whether references designate the same object. Use identity to investigate sharing, not to compare ordinary numeric or textual values. Implementation details such as reuse of some immutable objects should not become application assumptions.
2. Follow one nested mutation through three alternatives
Run this complete example. The assertions are part of the explanation: they identify the exact boundary that each operation changes.
from copy import copy, deepcopy
original = {"name": "Ada", "scores": [70, 80]}
alias = original
shallow = copy(original)
deep = deepcopy(original)
assert alias is original
assert shallow is not original
assert shallow["scores"] is original["scores"]
assert deep["scores"] is not original["scores"]
shallow["name"] = "Grace"
shallow["scores"].append(90)
assert original == {"name": "Ada", "scores": [70, 80, 90]}
assert deep == {"name": "Ada", "scores": [70, 80]}
print(original, shallow, deep, sep="\n")
Changing the shallow copy's name entry only changes that copy's dictionary. Appending a score changes the shared inner list. The deep copy's list is separate in this example, so it retains the original two scores. A list slice or a dictionary's copy() method also creates a shallow container copy; neither recursively separates all nested mutable state.
| Operation | New outer dictionary? | New scores list? | Intended use in this example |
|---|---|---|---|
| Assignment | No | No | Share the same record intentionally |
| Shallow copy | Yes | No | Change top-level fields while sharing scores |
| Deep copy | Yes | Yes | Experiment with the ordinary nested data separately |
The table describes this specific structure. A custom class can define its own copying behavior, and immutable objects may be reused. Do not turn the last row into a claim that every reachable object is universally duplicated.
3. Deep copying preserves relationships inside the copy
Consider a graph of objects in which two fields intentionally refer to the same list. Duplicating that list separately for each field would change the graph's meaning. Python's deep-copy machinery keeps a memo of objects it has already processed during one copying operation. This helps handle cycles and preserve such internal sharing.
from copy import deepcopy
shared = [1, 2]
source = {"left": shared, "right": shared}
copied = deepcopy(source)
assert copied["left"] is copied["right"]
assert copied["left"] is not shared
copied["left"].append(3)
assert copied["right"] == [1, 2, 3]
assert shared == [1, 2]
cycle = []
cycle.append(cycle)
cycle_copy = deepcopy(cycle)
assert cycle_copy is not cycle
assert cycle_copy[0] is cycle_copy
print("Internal sharing and the cycle were preserved.")
The copied record is isolated from the original list, but its two fields still share one copied list. That behavior is useful when the original alias is intentional. If the desired output instead requires independent lists for each field, construct that new structure explicitly. Isolation from the source and independence among every output field are separate contracts.
4. Copy only the state that the transformation owns
A small, explicit reconstruction is often clearer than applying deep copy to an entire application object. For example, a function that adds a score can return a new dictionary with a new scores list while retaining immutable text fields. This declares the changed boundary directly. It also makes later additions to the record easier to review: a newly added mutable field requires a conscious ownership decision.
Avoid using deep copy as a substitute for understanding resources. An open file, a database connection, or a socket represents external state and lifetime, not just nested values. Copying an object that holds such a resource does not establish an independent external resource. Some types cannot be copied by this mechanism at all. Separate plain input data from live handles before designing reproducible transformations.
A tuple also does not make its contents recursively immutable. A tuple containing a list cannot have its own slots reassigned, but the list can still change. Likewise, a frozen data record may prevent field assignment without preventing mutations inside a field. Decide whether the contract requires shallow immutability, immutable contents, or controlled access to mutable state.
5. Test the ownership promise, not only the returned value
For a function advertised as leaving its input unchanged, retain a suitable before-state, call the function, and compare the input afterward. Also change the returned nested data and verify whether that should affect the input. These tests exercise the promise that a simple value comparison of the initial output cannot establish.
An equality check alone can miss an alias: two equal lists may be distinct, or one list may be shared. Conversely, asserting that every inner object has a new identity can be unnecessarily restrictive when immutable objects or intentional internal sharing are allowed. Test only the boundaries the function promises to separate.
Create cases with an empty collection, two fields sharing the same inner list, and ordinary nested records. Include a cycle only if cyclic input is part of the supported domain. Unsupported structures should be rejected or documented rather than silently becoming a much broader API because one experiment happened to work.
6. Make the policy visible in the API
Document whether a function mutates in place, returns a shallow variant, or produces an independent transformation of supported plain data. Names such as append_score_in_place are useful when mutation is central to the operation. For larger workflows, one owner can control mutable state while other components receive immutable summaries or newly constructed records.
Copying also consumes work and memory. This article provides no measurement of either because the examples are too small to justify a general performance conclusion. Measure a representative object graph if copying becomes a bottleneck, including repeated references and the actual fields used. A smaller data contract can improve clarity before optimization is necessary.
The practical exercise is to explain every is assertion in the examples before running them, then replace the scores list with a tuple. Identify what becomes safe to share and what remains mutable. A defensible copying policy is an explanation of ownership, supported structure, and observable behavior, rather than a rule to always call one function.
Sources and scope
- Python 3.12: copy operations and memoization.
- Python 3.12: data model and object identity.
- Python 3.12: classes, names, and aliasing.
These primary references establish language behavior. The records, assertions, and ownership recommendations are original teaching examples; they are not measurements of application performance.