1

I am currently following "Python Crash Course" by Eric Matthes.

I am adding 30 new items to a list using a for loop. All the items are dictionaries. Then I am trying to update first few items of the list using the following code (actual code posted in the end)-

for item in items[0:3]:
    if item['someKey'] == 'someValue':
        item['someKey'] = 'someOtherValue'
        item['someOtherKey'] = 'someDifferentValue'

So, as I am changing only first 3 items, it should change only first 3 items. But it is changing all items in the list if I append to the list using variable declared outside the for loop while adding items in the first place.

#Case-1
items = []
dictionary = {'someKey': 'someValue', 'someOtherKey': 'someOtherValue'}
for item in range(30):
    items.append(dictionary)

If I run this code and later run the for loop to update some items, then all items in the list get modified. The slicing of the list with [0:3] does not work!

#Case-2
items = []
for item in range(30):
    dictionary = {'someKey': 'someValue', 'someOtherKey': 'someOtherValue'}
    items.append(dictionary)

So, in this case the update process works as expected. The for loop just updates only the first 3 items. Why does this happen? I have no idea! The list gets created in both the cases just fine. Only while modifying the already created list, the behaviour is differing.


Here's the actual code-

#Case-1
aliens = []
newAlien = {'color': 'green', 'speed': 'slow', 'points': 5}

for alienNumber in range(30):
   aliens.append(newAlien)

print(aliens) #Prints the whole list, showing adding dicts went just fine

for alien in aliens[0:3]: #intending change for only first 3 items
    if alien['color'] == 'green':
        alien['color'] = 'yellow'
        alien['speed'] = 'medium'
        alien['points'] = 10

for alien in aliens[0:5]:
    print(alien) #Shows all five items are modified even though intended for first 3

Here's where this went good-

#Case-2
aliens = []

for alienNumber in range(30):
    newAlien = {'color': 'green', 'speed': 'slow', 'points': 5}
    aliens.append(newAlien)

print(aliens) #prints whole list, 30 dicts are added

for alien in aliens[0:3]:
    if alien['color'] == 'green':
        alien['color'] = 'yellow'
        alien['speed'] = 'medium'
        alien['points'] = 10

for alien in aliens[0:5]:
    print(alien)  #Here only first 3 items are modified, as intended

Help me understand the behaviour of the the for loop here. The loop is only supposed to add items and nothing else. How declaring the new dictionary outside the for loop changes how items are modified later?

truth
  • 329
  • 3
  • 14
  • 1
    There is a canonical answer to this type of question. See dupe. You are not the first (or last) novice user that stumbles over this pitfall. – Patrick Artner Mar 28 '20 at 09:20
  • @PatrickArtner I don't think that is a good duplicate. That one is specifically about the list-repetition operator. Indeed, there is nothing about that operator that *necessitates* it to work that way, the implementation could copy the objects automatically for you. You are free to implement your own sequence type that does that. There probably is some better duplicate somewhere... – juanpa.arrivillaga Mar 28 '20 at 20:00
  • 1
    @truth probably you should read: https://nedbatchelder.com/text/names.html – juanpa.arrivillaga Mar 28 '20 at 20:01
  • @juanpa `newAlien = {'color': 'green', 'speed': 'slow', 'points': 5}`;`for alienNumber in range(30): aliens.append(newAlien)` adds the same dictionary reference multiple times into the list. If you change data through one reference it changes the underlying data that all references point to. It was a perfect duplicate. But having one more such question on SO does not matter, there are already thousands of them. – Patrick Artner Mar 28 '20 at 20:38
  • 1
    @PatrickArtner yes, but that duplicate isn't doing that, it's doing `[x]*y`, which isn't a consequence of how python references work, necessarily, rather, it is a consequence of how `__mul__` is implemented in lists. There's nothing in *the language* stopping `__mul__` from making copies. – juanpa.arrivillaga Mar 28 '20 at 21:12

3 Answers3

2

Don't think in terms of variables. Think in terms of objects.

In the case where you define (not declare - python doesn't really have variable declarations) the variable outside the loop, you create an object once and the variable keeps referring to the same object when you modify it. Then you keep adding the same object to the list

In the case where you define the variable inside, you create an object on each iteration, that new object is modified and added to the list each iteration.

juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
1

In case-1, You defined newAlien dictionary in outside the for loop so, when you add to the list then it will refer to the same object and when you change that object value from green to yellow in second for loop at that time it will change the value of that object. As per I said the same object is refer to the all Thus it will change all the value from green to yellow for all list.

In case-2, you defined newAlien dictionary in the loop so, in every iteration newAlien will refere to the new object so, when you change that value from green to yellow it will change that value for particuler elemnt only ca it is refering to different different object.

Try to print id of newAlien as below and check that in case-1 as refer it on same id and in case-2 refer it to different id

case-1

newAlien = {'color': 'green', 'speed': 'slow', 'points': 5}

for alienNumber in range(30):
   print(id(newAlien))
   aliens.append(newAlien)

case-2

for alienNumber in range(30):
    newAlien = {'color': 'green', 'speed': 'slow', 'points': 5}
    print(id(newAlien))
    aliens.append(newAlien)

Please run above both code and see the output of print statement

MK Patel
  • 1,354
  • 1
  • 7
  • 18
0

To get a better understanding, try executing the following code snippet:

#Case-3
aliens = []
newAlien = {'color': 'green', 'speed': 'slow', 'points': 5}

for alienNumber in range(30):
   aliens.append(newAlien)

print(aliens) #Prints the whole list, showing adding dicts went just fine

aliens[6]['color'] = 'red'
aliens[6]['points'] = 8
aliens[6]['speed'] = 'fast'

for alien in aliens[0:5]:
    print(alien) #Shows all five items are modified even though intended only for the 7th item.

Explanation

The reason this happens is not that you have modified all 30 dict items on the list. Actually, in this case, you have appended 30 object references of the newAlien dictionary into the aliens list. So, even if you modify one dict item, as I have tried doing in this case when you try to print all the 30 items, they will all be the same and hence it will seem like they have all been modified. When in reality, there is one newAlien dictionary object and you have modified that. Now, when you try to access it using any of the 30 object references, you will only print the modified object.

Even in Case-1, the if alien['color'] == 'green': condition is true only for the first item in the aliens list. The newAlien object is then modified and after that, the color attribute for all 30 items becomes yellow. You can verify that by printing a message inside the loop within the if clause, to see how many times the condition evaluated to true.

In Case-2, you create a new newAlien object every time the loop iterates and store the reference to them. Therefore, there are 30 distinct newAlien dictionaries and you changing one is independent of the others.

Akash Das
  • 163
  • 15