Noor is organizing a picnic. Her supply plan has two rows: food first, equipment second.
original = [["sandwiches", "fruit"], ["cups", "napkins"]]
She wants to try a different plan without disturbing this one. She creates a second variable, adds cookies to its food row, and discovers that the original now contains cookies too. She tries original.copy() next. The outer list is different, yet editing a row still changes both plans.
To understand the surprise, follow the objects those names point to. We will compare assignment, a shallow copy and a deep copy using the same small picnic plan. You should already know list indexing and append(). The examples use Python 3 and its standard library; you can also follow the results without running code.
Two names can belong to one plan
Start with assignment:
original = [["sandwiches", "fruit"], ["cups", "napkins"]]
alias = original
alias[0].append("cookies")
print(original)
print(alias is original)
The output is:
[['sandwiches', 'fruit', 'cookies'], ['cups', 'napkins']]
True
Assignment gave Noor another name for the same list. It did not make another list. Editing the food row through alias therefore changed the plan reached through original too. Python's copy documentation
Here, is asks whether two expressions refer to the same object. By contrast, == compares the values in these lists. Separate lists can have equal contents. Python identity comparisons
The next examples use assert to check these relationships. A successful check prints nothing; a false claim raises AssertionError. That lets us test the structure as well as print its contents.
A shallow copy gives Noor a new outer list
Reset the plan before trying another approach:
original = [["sandwiches", "fruit"], ["cups", "napkins"]]
trial = original.copy()
assert trial == original
assert trial is not original
assert trial[0] is original[0]
assert trial[1] is original[1]
list.copy() makes a shallow copy. Noor has a new outer list, but its entries still refer to the existing food and equipment rows. Python's list methods
Imagine two clipboards pointing to the same paper slips. Adding another clipboard does not photocopy the slips. An edit to a shared food slip is visible through either clipboard.
Compare these two operations:
trial.append(["blanket"])
assert len(trial) == 3
assert len(original) == 2
trial[0].append("cookies")
assert "cookies" in original[0]
The first append changes the outer trial list. Only that list gains a third row. The second append reaches the shared food row and changes it. Both plans can still reach that row.
For each edit, ask which exact list receives the change. “I changed the copy” leaves out the part that matters when a list contains other lists.
Replacing a row is a different operation
Start fresh again:
original = [["sandwiches", "fruit"], ["cups", "napkins"]]
trial = original.copy()
trial[0] = ["salad"]
assert trial[0] == ["salad"]
assert original[0] == ["sandwiches", "fruit"]
assert trial[0] is not original[0]
assert trial[1] is original[1]
Noor replaced the first entry in the outer trial list. That entry now points to a new salad row. She did not edit the old food row, so the original keeps its sandwiches and fruit.
The result is uneven: the food rows are separate, while the equipment row remains shared. A shallow copy can be useful when Noor wants to reorder or replace whole rows while keeping the row contents shared. It cannot give her fully independent row contents for every edit.
Copy the rows when the plans must be independent
Noor wants to change both food and equipment without changing the original plan. For these nested lists, deepcopy() supplies that independence:
from copy import deepcopy
original = [["sandwiches", "fruit"], ["cups", "napkins"]]
trial = deepcopy(original)
trial[0].append("cookies")
trial[1][0] = "reusable cups"
print(original)
print(trial)
assert trial is not original
assert trial[0] is not original[0]
assert trial[1] is not original[1]
The output shows that the original is unchanged:
[['sandwiches', 'fruit'], ['cups', 'napkins']]
[['sandwiches', 'fruit', 'cookies'], ['reusable cups', 'napkins']]
Deep copying works recursively through this structure, creating a new outer list and new row lists. The rows are mutable: their contents can change after creation. The strings inside them do not need independent editable copies. Shallow and deep copying
These arrows show references before any edits. The two rows in Noor's original plan are distinct lists.
For this particular two-level structure, Noor could also copy each row explicitly:
trial = [row.copy() for row in original]
The list comprehension builds an outer list from newly copied rows. This works here because those rows contain only strings. If another mutable list were nested inside a row, that deeper list would still be shared. Python's list comprehensions
Your turn
Work out both printed values before running this fresh example. Then change the copying line so that the original stays entirely unchanged.
plan = [["tea"], ["water"]]
trial = plan.copy()
trial[0].append("lemon")
trial[1] = ["juice"]
print(plan)
print(trial)
Show the worked answer
[['tea', 'lemon'], ['water']]
[['tea', 'lemon'], ['juice']]
The lemon was appended to a shared inner list. The juice assignment replaced only the second entry of trial.
For independent rows, import deepcopy and use trial = deepcopy(plan) in a fresh run. The original then stays [['tea'], ['water']]. In this two-level example, trial = [row.copy() for row in plan] would also work.
What to remember
Deep copying does not mean every position in a structure becomes unrelated. If the source contains the same row twice, deepcopy() keeps that relationship inside the copy using its record of copied objects. It also has limits for special objects and allows classes to customize copying. Copy behavior and its memo
The complete example script (the complete code is below) checks that repeated-row case as well as Noor's plan. For the everyday question, begin with the edit you intend to make: trace the outer list and its row references, then decide which objects need to be independent. A new name, a new outer list and independent rows are three different results.
Complete optional Python example
"""Original synthetic shallow/deep-copy demonstrations; Python stdlib only."""
from copy import deepcopy
def fresh_plan():
return [["sandwiches", "fruit"], ["cups", "napkins"]]
def main():
original = fresh_plan()
alias = original
assert alias is original
alias[0].append("cookies")
assert original[0] == ["sandwiches", "fruit", "cookies"]
original = fresh_plan()
trial = original.copy()
assert trial == original
assert trial is not original
assert trial[0] is original[0]
assert trial[1] is original[1]
trial.append(["blanket"])
assert len(trial) == 3
assert len(original) == 2
trial[0].append("cookies")
assert "cookies" in original[0]
original = fresh_plan()
trial = original.copy()
trial[0] = ["salad"]
assert trial[0] == ["salad"]
assert original[0] == ["sandwiches", "fruit"]
assert trial[0] is not original[0]
assert trial[1] is original[1]
original = fresh_plan()
trial = deepcopy(original)
trial[0].append("cookies")
trial[1][0] = "reusable cups"
assert original == fresh_plan()
assert trial == [["sandwiches", "fruit", "cookies"], ["reusable cups", "napkins"]]
assert trial is not original
assert trial[0] is not original[0]
assert trial[1] is not original[1]
# Deep copy preserves intentional aliasing *within* the copied structure.
row = ["shared"]
repeated = [row, row]
duplicate = deepcopy(repeated)
assert duplicate is not repeated
assert duplicate[0] is duplicate[1]
assert duplicate[0] is not row
duplicate[0].append("new")
assert duplicate == [["shared", "new"], ["shared", "new"]]
assert row == ["shared"]
original = fresh_plan()
trial = [row.copy() for row in original]
trial[0].append("cookies")
assert original == fresh_plan()
assert all(left is not right for left, right in zip(original, trial))
# Exercise: shared-row mutation versus outer-entry replacement.
plan = [["tea"], ["water"]]
trial = plan.copy()
trial[0].append("lemon")
trial[1] = ["juice"]
assert plan == [["tea", "lemon"], ["water"]]
assert trial == [["tea", "lemon"], ["juice"]]
print(plan)
print(trial)
plan = [["tea"], ["water"]]
trial = deepcopy(plan)
trial[0].append("lemon")
trial[1] = ["juice"]
assert plan == [["tea"], ["water"]]
assert trial == [["tea", "lemon"], ["juice"]]
print("After deep-copy exercise, original:", plan)
print("All nested-copy assertions passed.")
if __name__ == "__main__":
main()