0

python 3, new to coding and python. i built a class dictionary with default values, then attempted to build a nested dictionary based on that class dictionary and encountered unexpected behaviour:

class User:

    def __init__(self, *, name=None, age=None, hobbies=[]):
        self.name = name
        self.age = age
        self.hobbies = hobbies

counter = 0
class_dict = {}

# building the nested dicts with default values
for num in range(0, 3):
    """
    1. referencing "User.__init__.__kwdefaults__"
    vs writting the actual dict directly into the nested dict
    2. {"name": None, "age": None, "hobbies": []}
    """
    class_dict.update({f"key_{num}": User.__init__.__kwdefaults__})
    # class_dict.update({f"key_{num}": {"name": None, "age": None, "hobbies": []}})
print("Blue print: " + str(class_dict))


# updating values in the nested dicts
for x in range(0, 3):   # simplified loop
    dict_key = "key_" + str(counter)
    try:
        if 1 < 2:   # simplified if check
            class_dict[dict_key]["name"] = "updated" + str(counter)
            print("inside loop: " + str(class_dict))
            counter = counter + 1
    except:
        continue

print("<<< final result: " + str(class_dict) + ">>>")  # end-result
  1. the " User.init.kwdefaults " version will updated the correct nested dicts keys inside the loop as expected, but as end result all 3 nested dicts "name" key store "updated2" as value. what ever gets changed in the last iteration of the loop gets changed in all the nested dicts.

  2. the actual dict " {"name": None, "age": None, "hobbies": []} " version also updates the correct nested dict keys inside the loop as expected. however the end result here for the "name" key in nested dict 1 stores the value "updated0", in nested dict 2 "updated1" and in nested 2 "updated2".

the end result of 2. is what i was aiming for and it took me a while to find the issue. i don't understand why both versions behave identically inside the loop but net different end results. is there a dunder/magic method to reference the class dictionary and get version 2. as end result?

theoka
  • 53
  • 9
  • Note that there is only *one single object* ``User.__init__.__kwdefaults__``. You just keep adding this *one object* again and again. – MisterMiyagi Sep 03 '20 at 16:52
  • Note that since ``User.__init__.__kwdefaults__`` uses the defaults of ``User.__init__``, this is a convoluted case of mutable defaults of functions. – MisterMiyagi Sep 03 '20 at 16:53
  • Does this answer your question? [“Least Astonishment” and the Mutable Default Argument](https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument) – MisterMiyagi Sep 03 '20 at 16:57
  • Does this answer your question? [Create List of Dictionary Python](https://stackoverflow.com/questions/11492656/create-list-of-dictionary-python) – MisterMiyagi Sep 03 '20 at 17:00

2 Answers2

2

Reduced to a minimal example, your code boils down to:

d = {'name':None}

dicts = {1: d, 2:d}

print(dicts)
# {1: {'name': None}, 2: {'name': None}}

dicts[1]['name'] = 1
dicts[2]['name'] = 2

print(dicts)
# {1: {'name': 2}, 2: {'name': 2}}

which is not surprising, as dicts[1] and dicts[2] are one and the same dict d.

print(d)
# {'name': 2}

Note that if you called __init__ by creating some User(...), which you never do in the code in your question, you would meet the Least Astonishment” and the Mutable Default Argument problem.

Thierry Lathuille
  • 23,663
  • 10
  • 44
  • 50
1

The problem is that you are assigning the same subdict over and over again to every key as you could check by running this after your code:

for x in range(0, 2):   # simplified loop
    dict_key = "key_" + str(x)
    dict_key2 = "key_" + str(x+1)
    print(f'subdict of key {x} is subdict of key {x+1}: {class_dict[dict_key] is class_dict[dict_key2]}')

The output is:

subdict of key 0 is subdict of key 1: True
subdict of key 1 is subdict of key 2: True

A solution would be by using deep copies as:

import copy

class User:

    def __init__(self, *, name=None, age=None, hobbies=[]):
        self.name = name
        self.age = age
        self.hobbies = hobbies

counter = 0
class_dict = {}

# building the nested dicts with default values
for num in range(0, 3):
    """
    1. referencing "User.__init__.__kwdefaults__"
    vs writting the actual dict directly into the nested dict
    2. {"name": None, "age": None, "hobbies": []}
    """
    class_dict.update({f"key_{num}": copy.deepcopy(User.__init__.__kwdefaults__)})
    # class_dict.update({f"key_{num}": {"name": None, "age": None, "hobbies": []}})
print("Blue print: " + str(class_dict))
deponovo
  • 1,114
  • 7
  • 23