Python Deep Copy a List of Lists in Order to Avoid Making Edits to the Original Nested List Object

I stumbled across this recently when I thought I had made a copy instead of a reference to the original when copying a list of lists. After confirming with a colleague that I was indeed copying the list (and, as well, not making any inadvertent references to the original in any of the list manipulations down the line), we dug into Python more deeply to discover the following idiosyncrasy:

Python creates a reference to a list when assigning it to a new variable:

>>> list_1 = [1, 2, 3, 4]
>>> list_2 = list_1
>>> # change the first element in the referenced list
>>> list_2[0] = 5
>>> # the first list will reflect the change
>>> list_1
[5, 2, 3, 4]
>>> list_2
[5, 2, 3, 4]

This can generally be gotten around by creating a copy of the list instead of just assigning it to a new variable, when this is done, changes to the second instance of the list are unique and the first instance remains unaffected:

>>> list_1 = [1, 2, 3, 4]
>>> list_2 = list_1[:]
>>> # change the first element in the copied list
>>> list_2[0] = 5
>>> # the first list will remain the same
>>> list_1
[1, 2, 3, 4]
>>> list_2
[5, 2, 3, 4]

However, when the list is comprised of other lists, a copy of the outer list is made, however, the inner lists continue to contain references to the original and changes to the copied inner list will be reflected in the original:

>>> list_1 = [[1, 2, 3, 4], [5, 6, 7, 8]]
>>> list_2 = list_1[:]
>>> # change the first outer element in the copied list
>>> list_2[0] = 5
>>> # the first list will remain the same
>>> list_1
[[1, 2, 3, 4], [5, 6, 7, 8]]
>>> list_2
[5, [5, 6, 7, 8]]
>>> # change the first element of the nested list in the copied list
>>> list_2[1][0] = 'a'
>>> # the first list will reflect the change 
>>> list_1
[[1, 2, 3, 4], ['a', 6, 7, 8]]
>>> list_2
[5, ['a', 6, 7, 8]]

Since the outer list contains objects (the inner list, or nested list, is an object) a deep copy is required in order to safely make edits to the nested list without those changes being reflected in the original:

>>> import copy
>>> list_1 = [[1, 2, 3, 4], [5, 6, 7, 8]]
>>> list_2 = copy.deepcopy(list_1)
>>> # change the first outer element in the deep-copied list
>>> list_2[0] = 5
>>> # the first list will remain the same
>>> list_1
[[1, 2, 3, 4], [5, 6, 7, 8]]
>>> list_2
[5, [5, 6, 7, 8]]
>>> # change the first element of the nested list in the copied list
>>> list_2[1][0] = 'a'
>>> # the first list will still remain the same
>>> list_1
[[1, 2, 3, 4], [5, 6, 7, 8]]
>>> list_2
[5, ['a', 6, 7, 8]]

Incidentally, slicing the whole of the original list to make a copy (list_2 = list_1[:]) or calling the list() function on the original list to make a copy (list_2 = list(list_1)) have the same affects as calling copy.copy() on it to make the copy.

I use the term idiosyncrasy with some trepidation. Logically for me, it was an idiosyncrasy, in the grander aspects of the Python language, less so. I expected that when I specified a copy rather than a reassignment, a copy would happen completely. In retrospect, however, the difference makes sense and explicitly calling one of the functions makes the difference perfectly clear. I actually began this exercise not by copying the list with the copy function (as in the example shown above, which I've done for clarity), but by using the slice method which makes things less clear.