If you need a second list that starts equal to an existing one, the mechanism matters: plain assignment shares one object, a shallow copy gives a new list whose elements still point at the same inner objects, and a deep copy duplicates nested mutable data as well. This tutorial walks through those behaviors with short scripts you can run locally or with the in-page Run control where it appears. For list basics, see Python list.
The flow is assignment first, then the copy module, shallow copies (including slices), nested mutables, deepcopy, and a comparison table before the summary.
Tested on: Python 3.13.3; kernel 6.14.0-37-generic.
Copy a list with assignment (=)
In Python, new_list = old_list does not allocate a new list; it binds new_list to the same object as old_list. You can confirm that with id(old_list) == id(new_list).
Example: assignment shows the same object
Start from a small list and bind a second name to it, then compare identities so the output makes the sharing explicit.
my_list = [1, 2, 3, 4]
print("my_list contains:", my_list)
new_list = my_list
print("new_list contains:", new_list)
if id(my_list) == id(new_list):
print("Both names refer to the same list (same id).")
else:
print("Different ids.")Run it and you should see the same printed sequence for both names, then the message that both names share the same id.
Example: appending after assignment affects both names
Here the two names still point at one list, so an in-place method on either name updates the single underlying object.
my_list = [1, 2, 3, 4]
print("my_list:", my_list)
new_list = my_list
print("new_list:", new_list, "\n")
print("Appending to my_list")
my_list.append("a")
print("my_list after append:", my_list)
print("new_list after append:", new_list, "\n")
if id(my_list) == id(new_list):
print("Same id: one list, two names.")Appending to my_list also changes new_list, because they reference one list. That is the same idea as other in-place list changes such as removing elements from a list: every name bound to that object sees the update.
Example: mutating through one name updates the shared list
Assigning through a subscript on new_list still mutates that shared list, so my_list reflects the same index change.
my_list = [1, 2, 3, 4]
print("my_list:", my_list)
new_list = my_list
print("new_list:", new_list, "\n")
print("Assigning new_list[1] = 'TEST'")
new_list[1] = "TEST"
print("my_list after index assign:", my_list)
print("new_list after index assign:", new_list, "\n")
if id(my_list) == id(new_list):
print("Same id: index assignment mutates the shared list.")Replacing an element through new_list changes my_list as well, because both names point at the same list object.
The copy module
The standard library copy module supports shallow and deep copies of compound objects such as lists, tuples, dicts, and many user-defined types.
This small program builds a list that contains another list, takes a shallow and a deep copy, then mutates the inner list through the original. Watch how shallow follows the inner mutation while deep does not.
import copy
sample = [[1], 2]
shallow = copy.copy(sample)
deep = copy.deepcopy(sample)
sample[0].append(3)
print("after mutating inner list via sample:", sample)
print("shallow sees same inner list:", shallow)
print("deep stays independent:", deep)The last line should still show [[1], 2] for deep while sample and shallow show the inner list grown to [1, 3].
For lists you can often use list.copy() or a full slice [:] instead of copy.copy when you only need a shallow duplicate of a list.
Shallow and deep copy are not meant for every type: modules, class objects, functions, methods, tracebacks, stack frames, sockets, and similar objects are not copied in the usual sense. When a copy cannot be performed, the module raises copy.Error.
Shallow copy with copy.copy
A shallow copy constructs a new collection object and inserts references to the items found in the original. Top-level list slots are rehomed in a new list, but nested mutable objects (for example a dict inside the list) are not duplicated: both lists refer to the same inner object.
Example: shallow copy gets a new list id
For a flat list of immutables, copy.copy produces a new list object with the same values, so the two id values differ even though the printed contents match.
import copy
my_list = [1, 2, 3, 4]
print("my_list:", my_list)
new_list = copy.copy(my_list)
# Equivalent for lists: new_list = my_list[:] or new_list = my_list.copy()
print("new_list:", new_list, "\n")
if id(my_list) == id(new_list):
print("Same id.")
else:
print("Different ids: new top-level list object.")You should see matching values, then the “Different ids” branch, because copy.copy allocated a new list container.
Example: changing a top-level element in one list does not change the other
Replacing one slot rebinds only that slot in new_list; integers are immutable, so the other list’s elements are untouched.
import copy
my_list = [1, 2, 3, 4]
print("my_list:", my_list)
new_list = copy.copy(my_list)
print("new_list:", new_list, "\n")
print("Assign new_list[1] = 'TEST'")
new_list[1] = "TEST"
print("my_list after assign:", my_list)
print("new_list after assign:", new_list, "\n")
if id(my_list) == id(new_list):
print("Same id.")
else:
print("Different ids.")Here my_list stays [1, 2, 3, 4] with only the second slot in new_list changed, because replacing an integer in one list does not touch the other list’s slots. Appending to one list would likewise not append to the other, as long as you only mutate the list objects themselves, not shared nested objects. For more list APIs, see Python list pop() examples.
Example: nested dicts are still shared after a shallow copy
The outer list is new, but the first element is still the same dict object in memory, so in-place key updates on one list show up on the other.
import copy
my_list = [{"car": "maruti"}, 2, "apple"]
new_list = copy.copy(my_list)
print("Append to my_list")
my_list.append("TEST")
print("my_list:", my_list)
print("new_list:", new_list, "\n")
print("Mutate nested dict in my_list")
my_list[0]["car"] = "honda"
print("my_list:", my_list)
print("new_list:", new_list, "\n")
if id(my_list) == id(new_list):
print("Same top-level list id.")
else:
print("Different top-level list ids; check nested dict still shared.")The append only affects my_list, but changing my_list[0]["car"] also changes new_list[0]["car"], because the shallow copy reuses the same dict instance in both lists.
Deep copy with copy.deepcopy
copy.deepcopy walks the object graph and duplicates mutable objects recursively (with cycle detection and user hooks documented in the library reference). It is slower than a shallow copy but is the right tool when you need fully independent nested structures.
Example: deepcopy yields a new list with different inner dict ids
After deepcopy, the outer list and the nested dict should both be fresh objects, which you can verify with id on the lists and is on the dicts.
import copy
my_list = [{"car": "maruti"}, 2, "apple"]
print("my_list:", my_list)
new_list = copy.deepcopy(my_list)
print("new_list:", new_list, "\n")
if id(my_list) == id(new_list):
print("Same list id.")
else:
print("Different list ids.")
print("Same nested dict object?", my_list[0] is new_list[0])You should see different list ids and Same nested dict object? False, meaning the dict was duplicated, not aliased.
Example: mutating a nested dict after deepcopy does not leak
Changing the nested mapping on my_list must not alter new_list, because deepcopy stopped the two trees from sharing that dict.
import copy
my_list = [{"car": "maruti"}, 2, "apple"]
print("my_list:", my_list)
new_list = copy.deepcopy(my_list)
print("new_list:", new_list, "\n")
print("Mutate nested dict in my_list only")
my_list[0]["car"] = "honda"
print("my_list:", my_list)
print("new_list:", new_list)new_list should still show maruti for the nested car field, while my_list shows honda, because the deep copy detached the nested dict.
Assignment vs shallow copy vs deepcopy
Use this table as a quick mental model (see also Python functions for how you might wrap copy logic in reusable helpers):
| Behavior | Assignment (=) |
Shallow (copy.copy, [:], list.copy) |
deepcopy |
|---|---|---|---|
| New top-level list object | No | Yes | Yes |
| Same items at first level | Same object | Same references as original | New objects where the copier duplicates mutables |
| Nested mutable objects (e.g. dict in list) | Shared | Shared | Independent copies (by default) |
| Typical use | Another name for the same list | Duplicate a flat or immutable-heavy list | Duplicate trees with nested lists or dicts |
Summary
Assignment binds a new name to an existing list; mutations are visible through every alias. A shallow copy creates a new list but reuses references to the original items, so nested mutables stay linked unless you replace the item in one list with a different object. copy.deepcopy duplicates nested mutable structure so two list values can evolve independently. For flat lists of immutable values such as numbers and strings, shallow techniques are enough and cheaper; when in doubt about nesting, prefer deepcopy or restructure data so you do not need a full graph copy.
References
- Stack Overflow: shallow copy, deepcopy, and assignment
copy— Shallow and deep copy operations (Python 3)

