NOETRION

Choose a topic

TUTORIAL · 7 MIN READ

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.

Assignment, shallow copy, and deep copy have different sharing boundaries
An original conceptual diagram. A new outer container can still contain references to the original inner containers. Open full-size diagram.

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

These primary references establish language behavior. The records, assertions, and ownership recommendations are original teaching examples; they are not measurements of application performance.