6.2. Aliasing and Copying#
A variable does not hold a list directly; it holds a reference to a list object. This section looks at what that means: when two names refer to the same object, how a change made through one name shows up through the other, and how to make copies that are truly independent.
The examples use lists, but the same rules apply to every mutable collection, including sets, which you will meet in Section 7.4. Tuples, covered in Section 6.3, are immutable, so a change can never show up through an alias of a tuple itself.
The table below summarizes the three cases:
Concept |
Outer Object |
Inner Objects |
Syntax |
Changes affect original? |
|---|---|---|---|---|
Aliasing |
Same |
Same |
|
Yes |
Shallow Copy |
New |
Same |
|
Sometimes (when nested) |
Deep Copy |
New |
New |
|
No |
6.2.1. Objects and Values#
Consider these assignment statements:
a = 'banana'
b = 'banana'
Both a and b refer to a string, but it is not obvious whether they refer to the same string. There are two possible states, shown in the following figure.
In the diagram on the left, a and b refer to two different objects that have the
same value. In the diagram on the right, they refer to the same object.
To check whether two variables refer to the same object, you can use the is operator.
a = 'banana'
b = 'banana'
a is b
True
In this example, Python only created one string object, and both a
and b refer to it.
Python sometimes reuses one object for identical short strings, an optimization called interning, so a is b happens to be True here. Don’t rely on it: use == to compare values and is only to check whether two names refer to the same object.
But when you create two lists, you get two objects.
a = [1, 2, 3]
b = [1, 2, 3]
a is b
False
So the state diagram looks like this.
In this case, the two lists are equivalent, because they have the same elements, but not identical, because they are not the same object.
If two objects are identical, they are also equivalent, but if they are equivalent, they are not necessarily identical.
The is operator checks identity, whether two names refer to the same object, while == checks only whether the values are equivalent.
### Exercise: Objects and Values *(basic)*
x = [10, 20, 30]
y = x
z = [10, 20, 30]
# 1. Check if x and y refer to the same object using "is"
# 2. Check if x and z have the same value using "=="
# 3. Check if x and z refer to the same object using "is"
### Your code starts here.
### Your code ends here.
x is y: True
x == z: True
x is z: False
6.2.2. Aliasing#
Aliasing occurs when two or more variables point to the same object in memory. When you do a variable assignment using = in Python, you’re not copying the object; you’re creating another variable that points to the same object. This second variable is an alias.
If a refers to an object and you assign b = a, then both variables refer to the same object.
a = [1, 2, 3]
b = a
b is a
True
So the state diagram looks like this.
The association of a variable with an object is called a reference. In this example, there are two references to the same object.
An object with more than one reference has more than one name, so the object is aliased. If the aliased object is mutable, a change made through one name is visible through the other. In this example, changing the list through b also changes what you see through a, because both names refer to the same list.
b[0] = 5
a
[5, 2, 3]
In other words, a “sees” the change. This behavior can be useful, but it is error-prone. In general, avoid aliasing mutable objects unless you intend to share them.
You can confirm that two names share one object with is, or by comparing their id() values, which identify an object while it exists:
# Aliasing: both variables point to the same list
original = [1, 2, 3, 4, 5]
alias = original # NOT a copy!
alias[0] = 999 # Modify through the alias
print("Original:\t", original) # check to see if original is modified
print("Alias:\t\t", alias)
print("Same id?\t", id(original) == id(alias)) # True
print("Same object?\t", original is alias) # True
Original: [999, 2, 3, 4, 5]
Alias: [999, 2, 3, 4, 5]
Same id? True
Same object? True
For immutable objects like strings, aliasing is not as much of a problem. In this example:
c = 'banana'
d = 'banana'
It almost never makes a difference whether c and d refer to the same
string or not.
Reassigning a name is different from modifying an object. With immutable strings, reassigning one name creates a new object and never affects the other name:
# Contrast with immutable strings
name1 = "Chen"
name2 = name1 # alias: both names refer to the same string
print("Same object?", name1 is name2) # True
name2 = "Alice" # reassignment: name2 now refers to a new string
print(name1, name2) # name1 is unchanged
print("Same object?", name1 is name2) # False
Same object? True
Chen Alice
Same object? False
Aliasing is useful for efficiency (no copying needed), but can cause unexpected behavior if you modify mutable objects, thinking you have independent copies.
### Exercise: Aliasing *(applied)*
a = [1, 2, 3]
# 1. Create an alias b that points to the same list as a
# 2. Modify the list through b by changing the first element to 99
# 3. Create a true copy c using slicing
# 4. Modify c and observe that a remains unchanged
### Your code starts here.
### Your code ends here.
a after aliasing: [99, 2, 3]
b: [99, 2, 3]
Same object? True
a after copying: [99, 2, 3]
c: [99, 777, 3]
Same object? False
6.2.3. List Arguments in Functions#
When you pass a list to a function, the function gets a reference to the
list. If the function modifies the list, the caller sees the change. For
example, pop_first uses the list method pop to remove the first element from a list.
def pop_first(lst):
return lst.pop(0)
Here is how to use it:
letters = ['a', 'b', 'c']
pop_first(letters)
'a'
The return value is the first element, which has been removed from the list, as displaying the list shows:
letters
['b', 'c']
In this example, the parameter lst and the variable letters are aliases for the same object, so the state diagram looks like this:
Passing a reference to an object as an argument to a function creates a form of aliasing. If the function modifies the object, those changes persist after the function is done.
### Exercise: List Arguments in Functions *(applied)*
def add_item(lst, item):
"""Add item to list and return the list"""
lst.append(item)
return lst
# 1. Create a list with [1, 2, 3], call it original
# 2. Call add_item with your list and the value 4,
# save the result in a variable called updated
# 3. Print the original list to see if it changed after calling the function
# 4. check if original and updated refer to the same object using "is"
### Your code starts here.
### Your code ends here.
Original list: [1, 2, 3]
Returned list: [1, 2, 3, 4]
Original list after function call: [1, 2, 3, 4]
Same object? True
6.2.4. Shallow Copy#
You can make a shallow copy of a list in four ways: slicing with [:], the .copy() method, the list() function, or copy.copy() from the copy module.
import copy
a = [1, 2, 3]
b = a[:] # list slicing
b = a.copy() # copy() method
b = list(a) # list() function
b = copy.copy(a) # copy module
Each of these creates a new list object, but only the top level is copied. If the list contains mutable objects such as nested lists, the copy holds references to the same inner objects rather than copies of them.
For a flat list that holds only immutable values, such as numbers, strings, or Booleans, a shallow copy is all you need: each copy is a new list object, as the is checks below show.
# four ways to create a shallow copy
import copy
letters = ['a', 'b', 'c', 'd']
letters_copy1 = letters[:] # list slicing
letters_copy2 = letters.copy() # the copy() method
letters_copy3 = list(letters) # the list function
letters_copy4 = copy.copy(letters) # using the copy module
print(letters_copy1)
print(letters_copy2)
print(letters_copy3)
print(letters_copy4)
print("All copies have the same contents?", letters_copy1 == letters_copy2 == letters_copy3 == letters_copy4) # True, contents are the same
print("letters_copy1 is letters?", letters_copy1 is letters) # False, different objects in memory
print("letters_copy2 is letters?", letters_copy2 is letters) # False, different objects in memory
print("letters_copy3 is letters?", letters_copy3 is letters) # False, different objects in memory
print("letters_copy4 is letters?", letters_copy4 is letters) # False, different objects in memory
['a', 'b', 'c', 'd']
['a', 'b', 'c', 'd']
['a', 'b', 'c', 'd']
['a', 'b', 'c', 'd']
All copies have the same contents? True
letters_copy1 is letters? False
letters_copy2 is letters? False
letters_copy3 is letters? False
letters_copy4 is letters? False
# a simpler example: changing a shallow copy of a flat list
original = [1, 2, 3, 4, 5]
shallow = original[:] # Creates a new list
print("Same value?\t", original == shallow) # True
print("Same object?\t", original is shallow) # False
# Modify the shallow copy
shallow[4] = 999
print("update shallow:\t", shallow)
print("Original:\t", original) # Original is unchanged
print("Shallow:\t", shallow) # Only the copy is modified
Same value? True
Same object? False
update shallow: [1, 2, 3, 4, 999]
Original: [1, 2, 3, 4, 5]
Shallow: [1, 2, 3, 4, 999]
However, for nested lists (lists containing other lists), shallow copy shares references to the nested objects:
# Using copy.copy() with nested lists
import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
print("original-shallow same value?\t", original == shallow) # True - contents are the same
print("original-shallow same object?\t", id(original) == id(shallow)) # False - different list objects
shallow[0].append(99)
print("update shallow (99):\t\t", shallow)
print("is original updated?\t\t", original) # [[1, 2, 99], [3, 4]] - nested list affected!
print("same nested object?\t\t", shallow[0] is original[0]) # True - same nested list object
original-shallow same value? True
original-shallow same object? False
update shallow (99): [[1, 2, 99], [3, 4]]
is original updated? [[1, 2, 99], [3, 4]]
same nested object? True
The two outer lists are different objects, but they share the same inner lists, so a change to shallow[0] also shows up in original[0]. The same thing happens when you make a shallow copy with slicing:
# Using slicing with nested lists
original = [[1, 2, 3], [4, 5, 6]]
shallow = original[:] # or original.copy()
# Modify the nested list
shallow[0][0] = 999
print("Original:", original) # Original is also modified!
print("Shallow:", shallow)
print("Same nested object?", shallow[0] is original[0]) # True - same nested list object
Original: [[999, 2, 3], [4, 5, 6]]
Shallow: [[999, 2, 3], [4, 5, 6]]
Same nested object? True
As you can see, modifying the nested list in shallow also affects original because they share references to the same inner lists.
6.2.5. Deep Copy#
To make a deep copy, use deepcopy() from the copy module. It copies the outer list and, recursively, every object nested inside it, so the copy is completely independent of the original.
import copy
original = [[1, 2, 3], [4, 5, 6]]
deep = copy.deepcopy(original)
print("Original:", original) # Original is unchanged
print("Deep copy:", deep) # Only the deep copy is modified
print("original-deep same value?", original == deep) # True - contents are the same
print("original-deep same object?", id(original) == id(deep)) # False - different list objects
print("same nested object?", deep[0] is original[0]) # False - different nested list objects
# Modify the nested list
deep[0][0] = 999
print("Original:", original) # Original is unchanged
print("Deep copy:", deep) # Only the deep copy is modified
Original: [[1, 2, 3], [4, 5, 6]]
Deep copy: [[1, 2, 3], [4, 5, 6]]
original-deep same value? True
original-deep same object? False
same nested object? False
Original: [[1, 2, 3], [4, 5, 6]]
Deep copy: [[999, 2, 3], [4, 5, 6]]
# simpler example with deep copy
import copy
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
print(original is deep) # False - different outer lists
deep[0].append(99)
print(original) # [[1, 2], [3, 4]] - original unchanged!
print(deep) # [[1, 2, 99], [3, 4]] - only deep copy changed
print(deep[0] is original[0]) # False - different nested list objects
False
[[1, 2], [3, 4]]
[[1, 2, 99], [3, 4]]
False
### Exercise: Shallow vs. Deep Copy *(applied)*
import copy
teams = [['Ann', 'Bo'], ['Cy', 'Di']]
# 1. Make a shallow copy of teams called shallow and a deep copy called deep
# 2. Append 'Ed' to the first team in shallow
# 3. Append 'Fay' to the first team in deep
# 4. Print teams, shallow, and deep.
# Which change shows up in teams, and why?
### Your code starts here.
### Your code ends here.
teams: [['Ann', 'Bo', 'Ed'], ['Cy', 'Di']]
shallow: [['Ann', 'Bo', 'Ed'], ['Cy', 'Di']]
deep: [['Ann', 'Bo', 'Fay'], ['Cy', 'Di']]
6.2.6. Summary#
==compares values;ischecks whether two names refer to the same object.Assignment creates an alias, not a copy. When the object is mutable, a change made through one name shows up through the other.
Passing a list to a function passes a reference, so the function can modify the caller’s list.
A shallow copy (slicing,
copy(),list(), orcopy.copy()) creates a new outer list but shares any nested objects.copy.deepcopy()also copies nested objects, so the copy is fully independent.
6.2.7. Further Reading#
Objects, values and types, Python Language Reference
Why did changing list ‘y’ also change list ‘x’?, Python Programming FAQ
copy: Shallow and deep copy operations, Python Standard Library documentation