What you are observing has nothing to do with dictionaries at all. You are getting confused by the difference between binding and mutation.
Let's forget dictionaries at first, and demonstrate the issue with simple variables. Once we understand the fundamental point, we can then go back to the dictionary example.
a = 1
b = a
a = 2
print(b) # prints 1
- On the first line you create a binding between the name
a
and the object 1
.
- On the second line you create a binding between the name
b
and the value of the expression a
... which is the very same object 1
which was bound to the name a
on the previous line.
- On the third line you create a binding between the name
a
and the object 2
, in the process forgetting that there ever was a binding between a
and the 1
.
It is vital to note that this last step cannot in any way affect b
!
The situation is completely symmetric, so if line 3 were b = 2
this would have absolutely no effect on a
.
Now, people often mistakenly claim that this is somehow a result of the immutability of integers. Integers are immutable in Python, but that is completely irrelevant. If we do something similar with some mutable objects, say lists, then we get equivalent results.
a = [1]
b = a
a = [2]
print(b) # prints [1]
Once again
a
is bound to some object
b
is bound to the same object
a
is now rebound to some different object
This cannot affect b
or the object to which it is bound [*] in any way! No attempt has been made anywhere to mutate any object, so mutability is completely irrelevant to this situation.
[*] actually, it does change the reference count of the object (at least in CPython) but that's not really an observable property of the object.
However, if, instead of rebinding a
, we
- Use
a
to access the object to which it is bound
- Mutate that object
then we will affect b
, because the object to which b
is bound will be mutated:
a = [1]
b = a
a[0] = 2
print(b) # prints [2]
In summary, you have to understand
The difference between binding and mutation. The former affects a variable (or more generally a location) while the latter affects an object. Therein lies the key difference
Rebinding a name (or location in general) cannot affect the object to which that name was previously bound (beyond changing its reference count).
Now, in your example you create something that looks (conceptually) like this:
a ---> { 'three' ----------------------> 3
'two' -------------> 2 ^
'one' ---> 1 } ^ |
^ | |
| | |
b ---> { 'one' ----- | |
'two' --------------- |
'three' -------------------------
and then a['one'] = 5
simply rebinds the location a['one']
so that it is no longer bound to the 1
but to 5
. In other words, that arrow coming out of the first 'one'
, now points somewhere else.
It is important to remember that this has absolutely nothing to do with the immutability of integers. If you make each and every integer in your example mutable (for example by replacing it with a list which contains it: i.e. replace every occurance of 1
with [1]
(and similarly for 2
and 3
)) then you will still observe essentially the same behaviour: a['one'] = [1]
will not affect the value of b['one']
.
Now, in this latest example, where the values stored in your dictionary are lists and therefore structured, it becomes possible to distinguish between shallow and deep copy:
b = a
will not copy the dictionary at all: it will simply make b
a new binding to the same single dictionary
b = copy.copy(b)
will create a new dictionary with internal bindings to the same lists. The dictionary is copied but its contents (below the top level) are simply referenced by the new dictionary.
b = copy.deepcopy(a)
will also create a new dictionary, but it will also create new objects to populate that dictionary, rather than referencing the original ones.
Consequently, if you mutate (rather than rebind) something in the shallow copy case, the other dictionary will 'see' mutation, because the two dictionaries share objects. This does not happen in the deep copy.