Dictionary behaves like a global variable

Question:

A dictionary that I pass as an argument seems to behave like a global variable. This was a surprise to me but it seems that it makes sense in Python (see for instance this topic). However, I’m very surprised that I get a different behavior for a different type of variable. Let assume that I have a script main.py that calls two functions from a module.

import mymodule

an_int = 42
a_dict = {'value':42}

mymodule.print_and_reassign(a_dict, an_int)
mymodule.print_again(a_dict, an_int)

with my modules.py containing the following functions

def print_and_reassign(a_dict, an_int):

    # Dictionary
    print(f"Dictionary value: {a_dict['value']}")
    a_dict['value']=970

    # Integer
    print(f"Integer value: {an_int}")
    an_int=970


def print_again(a_dict, an_int):

    # Dictionary
    print(f"Dictionary value: {a_dict['value']}")

    # Integer
    print(f"Integer value: {an_int}")

By running main.py, I get the following results

Dictionnary value: 42
Integer value: 42
Dictionnary value: 970
Integer value: 42

This is very counterintuitive to me. Why does the dictionary changed while the integer remains unchanged?

Asked By: Antoine T

||

Answers:

Calling a_dict['value'] = 970 underneath manipulates a_dict by calling its __setitem__ method, therefore a_dict‘s contents change.

Calling an_int = 970 assigns a new value to a local an_int variable, shadowing the value passed as a parameter.

Answered By: ljmc

As @jonsharpe said above,

Dictionaries are mutable, integers aren’t. And assigning a new value to the local variable is fundamentally different to calling a method on the value it refers to

To reinforce jon’s point, try reassigning the variable a_dict inside the function like so: a_dict = {"value": 970}.

def print_and_reassign(a_dict, an_int):
    print(f"Dictionary value before: {a_dict['value']}")
    a_dict = {"value": 970}
    print(f"Dictionary value after: {a_dict['value']}")

    print(f"Integer value before: {an_int}")
    an_int = 970
    print(f"Integer value after: {an_int}")
    
def print_again(a_dict, an_int):
    # Dictionary
    print(f"Dictionary value outside: {a_dict['value']}")

    # Integer
    print(f"Integer value outside: {an_int}")

Now, when you run your code you see that a_dict is not modified outside the function.

Dictionary value before: 42
Dictionary value after: 970
Integer value before: 42
Integer value after: 970
Dictionary value outside: 42
Integer value outside: 42

When you assign a new value to the name a_dict inside the function, it isn’t reflected outside the function. This is what happens when you do an_int = 970. Since integers are immutable, this is also what would happen if you did something like an_int += 100.

When you do a_dict['value'] = 970, you modify the value that is referred to by a_dict. Since dicts are mutable, this modification doesn’t need a reassignment so it shows in every variable refers to this value.

Answered By: Pranav Hosangadi

Yes, that’s a quite confusing part of programming in Python (or, rather, programming in general).

What happens is essentially that what you have in the beginning is this:

enter image description here

When calling the function, the variables inside the function’s scope are actually something different than the outer ones, even though you named them the same and they initially have the same value:

enter image description here

Then you change them, and this happens:

enter image description here

And after the function returns and its scope is destroyed, you are left with this:

enter image description here

Answered By: Jan Pokorný

The fact that there are sets, dictionaries and lists along with integers, strings and floats is a common reason for confusion.

What is the difference between the first group compared to the second? This mind boggling question comes up again and again in myriads of different flavors. So maybe it needs one more answer next to all the existing ones? I think yes, it needs and this motivates me to provide one below in the hope it will be helpful in resolving the confusion.

It is not the dictionary passed to function which shows ‘strange’ behavior. To get to the core of the reason for experiencing counter-intuitive behavior I will compare here the behavior of a Python integer vs a Python list.

Below a code snippet which demonstrates that you can change values in the list using for this purpose another identifier/reference. This can’t be done with an identifier representing an integer value:

L    = [3]
id_L_before = id(L)
L[0] =  4 # Changing a value in the list don't change the list id()
assert id(L) == id_L_before
# ---
P = L # using another identifier/reference to the list object 
P[0] = 5 #    changing a value in the "other" P list
assert L[0] == 5 #   changes "magically" the value in the list L
# ---
# You can't do the same with an identifier/reference to an int value: 
n = 3
id_n_before = id(n)
n = 4
assert id(n) != id_n_before # Changing the value changes the id()
m = n
m = 5 #     changing the value in the "other" int m
assert n == 4 #  DOES NOT change "magically" the value in the int n

The code above along with the explanations given as code comment should be enough to sufficiently answer the question.

If you want to see more of the strange and counter-intuitive aspects of Python just visit The Python’s Horror Show . which purpose is

to mess with your head but some people have reported strange new Python knowledge as a secondary effect

Here an excerpt:

d = {1: 'a', True: 'b', 1.0: 'c'}
>>> d
{1: 'c'}
Answered By: Claudio
Categories: questions Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.