L1 = [5, 6, 7]
L2 = L1 #L1 and L2 are aliases
#changing the contents of L1 <=> changing the contents of L2
#since L1 and L2 refer to the same address in memory
L2[0] = 42 #L1[0] changes as well
L2 = 42 #L1[0] doesn't change
#L2 now refers to 42 instead of the list [42, 6, 7]: that change
#is reflected in the GLOBALS table
#
#This is analgous to
#a = 5
#b = a
#a = 42 #b still refers to 5
We can make a shallow copy of L1 using
L1 = [5, 6, 7]
L2 = L1[:] #same as L2 = [L1[0], L1[1], L1[2]]
Here, we are creating a new list, in a new location in memory, whose contents are the same as the contents of L2.
Changing the contents of L2 (e.g., L2[0] = 42) is not the same as changing the contents of L1.
Consider the following:
L1 = [[1, 2], [3, 4]]
L2 = L1[:] #same as L2 = [L1[0], L1[1]]
Here, we have create a new list L2, but we haven't created new copies of the lists L1[0] and L1[1].
L1[0] refers to the same memory address as L2[0]
That is, id(L1[0]) == id(L2[0]).
In other words,L1[0] and L2[0] are aliases.
So modifying the contents of L1[0] is the same as modifying the contents of L2[0].
For example, L1[0][0] = 5 is the same as L2[0][0] = 5.
On the other hand, just like before, L1[0] = 5 is not the same as L2[0] = 5 -- L1 and L2 are two separate lists.
Here is a way to create a deep copy of L1 (if L1 is a list of lists of two ints):
L1 = [[1, 2], [3, 4]]
L2 = [[L1[0][0], L1[0][1]], [L1[1][0], L1[1][1]]]
Now, we've create 3 new lists when creating L2: the two inner lists, and the outer list. L1 and L2 are completely disconnected.
L2 = [[L1[0][0], L1[0][1]], [L1[1][0], L1[1][1]]]
L1[1][0] = 7
print("L1 = ", L1)
print("L2 = ", L2)
Remember that we did shallow copy of lists by repeatedly appending elements from the old list to the new list:
L1 = [[1, 2], [3, 4]]
L2 = []
for sublist in L1:
L2.append(sublist)
The above is a shallow copy: we only create a single new list, L2. After the process ends, L1[0] and L2[0], as well as L1[1] and L2[1] are aliases.
However, there is a quick fix here: just use sublist[:]. That creates a new copy of the list that sublist refers to:
L1 = [[1, 2], [3, 4]]
L2 = []
for sublist in L1:
L2.append(sublist[:])
Now, we've created a deep copy of L1, without specifying the lengths of the outer list, or the inner lists. Every time we execute
L2.append(sublist[:])
a new list is put in memory, and is then appended to L2 as its new last element.

Suppose the list is L1 = [[[1]]]. Can we make a deep copy of that list using the previous method (without modifications)? No.
L1 = [[[1]]]
L2 = []
for sublist in L1:
L2.append(sublist[:])
L2[0][0][0]=5
L1[0][0][0]
The problem is that we've created a new list whose element is a copy of [[1]]. However, we never made a copy of the innermost list, [1]. If its contents are modified, that has an effect everywhere.