107

Let's say we have a Python dictionary d, and we're iterating over it like so:

for k,v in d.iteritems():
    del d[f(k)] # remove some item
    d[g(k)] = v # add a new item

(f and g are just some black-box transformations.)

In other words, we try to add/remove items to d while iterating over it using iteritems.

Is this well defined? Could you provide some references to support your answer?

(It's pretty obvious how to fix this if it's broken, so this isn't the angle I am after.)

Bach
  • 5,939
  • 7
  • 30
  • 59
NPE
  • 464,258
  • 100
  • 912
  • 987
  • 1
    possible duplicate of [python - why is it not safe to modify sequence being iterated on?](http://stackoverflow.com/questions/3346696/python-why-is-it-not-safe-to-modify-sequence-being-iterated-on) – matt b Jul 21 '11 at 14:18
  • 1
    See http://stackoverflow.com/questions/5384914/deleting-items-from-a-dictionary-while-iterating-over-it – Todd Moses Jul 21 '11 at 14:21
  • I have tried to do this and it seems that if you leave initial dict size unchanged - e.g. replace any key/value instead of removing them then this code will not throw exception – Artsiom Rudzenka Jul 21 '11 at 14:34
  • 1
    I disagree that it's “pretty obvious how to fix this if it's broken” for everyone searching for this topic (including myself), and I wish the accepted answer had at least touched on this. – Alex Peters May 23 '19 at 11:26
  • Does this answer your question? [How to remove a key:value pair whereever the chosen key occurs in a deeply nested dictionary?](https://stackoverflow.com/questions/68508611/how-to-remove-a-keyvalue-pair-whereever-the-chosen-key-occurs-in-a-deeply-neste) – questionto42standswithUkraine Aug 29 '21 at 18:47
  • @questionto42 I don't understand the direction of your duplicate suggestion. The linked question is already closed as a duplicate, and if it wasn't, the direction of the targets should be the other way round anyway. This question has more views, more answers, and the question and answers are all much more highly scored. – cigien Aug 31 '21 at 01:41
  • @cigien The closing of the other Q should be unimportant. This question here is outdated in any regard. ".iteritems()" is from Python 2, the question is from 2011. But also with ".items()" from Python3, it does not have a working solution: the votes of the accepted answer are lower than those of the question, a typical sign that the question is not answered well enough. The accepted answer just says that answering this is impossible, a comment under it shows that the code snippet of the question leads to inconsistencies. It is agreed on meta to replace such old Q/A with new ones. – questionto42standswithUkraine Aug 31 '21 at 08:11
  • You cannot loop-change a dict without using an additional (recursive) function. This question must have a "*No*" as its only right answer. Yet: most of the searchers try to find a solution instead. This Q should be linked to a question that has it: [How can I remove a key:value pair wherever the chosen key occurs in a deeply nested dictionary?](https://stackoverflow.com/a/68508612/11154841) (= "delete"). Also helpful: [How can I replace a key:value pair by its value wherever the chosen key occurs in a deeply nested dictionary?](https://stackoverflow.com/a/68471198/11154841) (= "replace"). – questionto42standswithUkraine Aug 31 '21 at 08:58

9 Answers9

69

Alex Martelli weighs in on this here.

It may not be safe to change the container (e.g. dict) while looping over the container. So del d[f(k)] may not be safe. As you know, the workaround is to use d.copy().items() (to loop over an independent copy of the container) instead of d.iteritems() or d.items() (which use the same underlying container).

It is okay to modify the value at an existing index of the dict, but inserting values at new indices (e.g. d[g(k)]=v) may not work.

Janus Troelsen
  • 19,210
  • 14
  • 126
  • 189
unutbu
  • 777,569
  • 165
  • 1,697
  • 1,613
  • 3
    I think this is a key answer for me. A lot of use cases will have one process inserting things and another cleaning things up/deleting them so the advice to use d.items() works. Python 3 caveats not withstanding – easytiger Apr 26 '13 at 08:25
  • 4
    More information about the Python 3 caveats can be found in [PEP 469](http://legacy.python.org/dev/peps/pep-0469/#lists-as-mutable-snapshots) where the semantic equivalents of the aforementioned Python 2 dict methods are enumerated. – Lionel Brooks Sep 14 '14 at 21:16
  • 1
    *"It is okay to modify the value at an existing index of the dict"* -- do you have a reference for this? – Jonathon Reinhart Jan 18 '19 at 19:54
  • 2
    @JonathonReinhart: No, I don't have a reference for this, but I think it is pretty standard in Python. For example, Alex Martelli was a Python core developer and [demonstrates its usage here](https://stackoverflow.com/a/2315529/190597). – unutbu Jan 18 '19 at 20:35
60

It is explicitly mentioned on the Python doc page (for Python 2.7) that

Using iteritems() while adding or deleting entries in the dictionary may raise a RuntimeError or fail to iterate over all entries.

Similarly for Python 3.

The same holds for iter(d), d.iterkeys() and d.itervalues(), and I'll go as far as saying that it does for for k, v in d.items(): (I can't remember exactly what for does, but I would not be surprised if the implementation called iter(d)).

Craig McQueen
  • 39,646
  • 28
  • 118
  • 178
Raphaël Saint-Pierre
  • 2,400
  • 1
  • 18
  • 23
  • 67
    I will embarrass myself for the sake of the community by stating that I used the very code snippet. Thinking that since I didn't get a RuntimeError I thought everything was good. And it was, for a while. Anally retentive unit tests were giving me the thumbs up and and it was even running well when it was released. Then, I started getting bizarre behavior. What was happening was that items in the dictionary were getting skipped over and so not all items in the dictionary were being scanned. Kids, learn from the mistakes that I have made in my life and just say no! ;) – Alan Cabrera Jul 18 '15 at 16:43
  • 3
    Can I run in to problems if I'm changing the value at the current key (but not adding or removing any keys?) I would imaaaagine that this shouldn't cause any problems, but I'd like to know! – Gershom Maes Nov 11 '15 at 17:00
  • @GershomMaes I don't know of any, but you may still be running into a minefield should your loop body make use of the value and not expecting it to change. – Raphaël Saint-Pierre Nov 16 '15 at 22:32
  • 4
    `d.items()` should be safe in Python 2.7 (the game changes with Python 3), as it makes what is essentially a copy of `d`, so you're not modifying what you're iterating over. – Paul Price May 21 '16 at 05:40
  • It would be interesting to know if this is also true for `viewitems()` – jlh Jun 28 '18 at 13:06
  • @jlh it is ! Refer to https://docs.python.org/3/library/stdtypes.html#dictionary-view-objects. – Raphaël Saint-Pierre Jul 06 '18 at 14:47
33

You cannot do that, at least with d.iteritems(). I tried it, and Python fails with

RuntimeError: dictionary changed size during iteration

If you instead use d.items(), then it works.

In Python 3, d.items() is a view into the dictionary, like d.iteritems() in Python 2. To do this in Python 3, instead use d.copy().items(). This will similarly allow us to iterate over a copy of the dictionary in order to avoid modifying the data structure we are iterating over.

murgatroid99
  • 17,055
  • 9
  • 55
  • 92
  • 2
    FYI, the literal translation (as e.g. used by `2to3`) of Py2's `d.items()` to Py3 is `list(d.items())`, although `d.copy().items()` is probably of comparable efficiency. – Søren Løvborg Jul 24 '13 at 10:26
  • 2
    If the dict object is very large, is d.copy().items() efficiet? – dragonfly Oct 23 '13 at 09:50
  • @dragonfly I guess that's a rhetorical question? An alternate workaround suggested in other answers is to collect the changes you want to perform to a new list, and then separately apply those changes when you are out of the loop. If you need to change only some of the entries in the dictionary, this might require less memory (obviously also depending on the complexity of the individual values). – tripleee Apr 24 '22 at 07:59
15

I have a large dictionary containing Numpy arrays, so the dict.copy().keys() thing suggested by @murgatroid99 was not feasible (though it worked). Instead, I just converted the keys_view to a list and it worked fine (in Python 3.4):

for item in list(dict_d.keys()):
    temp = dict_d.pop(item)
    dict_d['some_key'] = 1  # Some value

I realize this doesn't dive into the philosophical realm of Python's inner workings like the answers above, but it does provide a practical solution to the stated problem.

2cynykyl
  • 903
  • 9
  • 12
  • Note that `dict.copy()` is not a deep copy so it doesn't really matter what's in your dict since the values won't be copied. – philippjfr Jun 22 '21 at 09:36
6

The following code shows that this is not well defined:

def f(x):
    return x

def g(x):
    return x+1

def h(x):
    return x+10

try:
    d = {1:"a", 2:"b", 3:"c"}
    for k, v in d.iteritems():
        del d[f(k)]
        d[g(k)] = v+"x"
    print d
except Exception as e:
    print "Exception:", e

try:
    d = {1:"a", 2:"b", 3:"c"}
    for k, v in d.iteritems():
        del d[f(k)]
        d[h(k)] = v+"x"
    print d
except Exception as e:
    print "Exception:", e

The first example calls g(k), and throws an exception (dictionary changed size during iteration).

The second example calls h(k) and throws no exception, but outputs:

{21: 'axx', 22: 'bxx', 23: 'cxx'}

Which, looking at the code, seems wrong - I would have expected something like:

{11: 'ax', 12: 'bx', 13: 'cx'}
combatdave
  • 745
  • 7
  • 17
  • I can understand why you might expect `{11: 'ax', 12: 'bx', 13: 'cx'}` but the 21,22,23 should give you clue as to what actually happened: your loop went through items 1, 2, 3, 11, 12, 13 but didn't manage to pick up the second round of new items as they got inserted in front of the items you had already iterated over. Change `h()` to return `x+5` and you get another x: `'axxx'` etc. or 'x+3' and you get the magnificent `'axxxxx'` – Duncan Jul 21 '11 at 14:39
  • Yeah, my mistake I'm afraid - my expected output was `{11: 'ax', 12: 'bx', 13: 'cx'}` as you said, so I'll update my post about it. Either way, this is clearly not well defined behaviour. – combatdave Jul 21 '11 at 15:12
4

Python 3 you should just:

prefix = 'item_'
t = {'f1': 'ffw', 'f2': 'fca'}
t2 = dict() 
for k,v in t.items():
    t2[k] = prefix + v

or use:

t2 = t1.copy()

You should never modify original dictionary, it leads to confusion as well as potential bugs or RunTimeErrors. Unless you just append to the dictionary with new key names.

Dexter
  • 5,882
  • 17
  • 70
  • 99
1

I got the same problem and I used following procedure to solve this issue.

Python List can be iterate even if you modify during iterating over it. so for following code it will print 1's infinitely.

for i in list:
   list.append(1)
   print 1

So using list and dict collaboratively you can solve this problem.

d_list=[]
 d_dict = {} 
 for k in d_list:
    if d_dict[k] is not -1:
       d_dict[f(k)] = -1 # rather than deleting it mark it with -1 or other value to specify that it will be not considered further(deleted)
       d_dict[g(k)] = v # add a new item 
       d_list.append(g(k))
brasofilo
  • 24,660
  • 15
  • 89
  • 174
Zeel Shah
  • 462
  • 6
  • 15
  • I'm not sure if it is safe to modify a list during iteration (although it may work in some cases). See [this question](https://stackoverflow.com/questions/3752618/python-adding-element-to-list-while-iterating) for example... – Roman Sep 17 '18 at 06:44
  • @Roman If you want to delete elements of a list, you can safely iterate over it in reverse order, since in normal order the index of the next element would change upon deletion. [See this example.](https://tio.run/##dY1BCsIwFETX5hQfREh2VXeFHEDoDYpIIRMNxKT8RrGnj2ms4MbZDfNmZpzTLYZjzls6JfCQQC4Q4wmeYCiyAQsDSwiPe80v31B2qhVUZCOXksHrtyp5CFdIj1A4tZKLZgdvPnzuSJN3U1rhfaOUqGvL0p/HjbPU9e68O2jdtAa@OjGyC6kA@Q0) – mbomb007 Dec 31 '18 at 20:22
1

This question asks about using an iterator (and funny enough, that Python 2 .iteritems iterator is no longer supported in Python 3) to delete or add items, and it must have a No as its only right answer as you can find it in the accepted answer. Yet: most of the searchers try to find a solution, they will not care how this is done technically, be it an iterator or a recursion, and there is a solution for the problem:

You cannot loop-change a dict without using an additional (recursive) function.

This question should therefore be linked to a question that has a working solution:

By the same recursive methods, you will also able to add items as the question asks for as well.


Since my request to link this question was declined, here is a copy of the solution that can delete items from a dict. See How can I remove a key:value pair wherever the chosen key occurs in a deeply nested dictionary? (= "delete") for examples / credits / notes.

import copy

def find_remove(this_dict, target_key, bln_overwrite_dict=False):
    if not bln_overwrite_dict:
        this_dict = copy.deepcopy(this_dict)

    for key in this_dict:
        # if the current value is a dict, dive into it
        if isinstance(this_dict[key], dict):
            if target_key in this_dict[key]:
                this_dict[key].pop(target_key)

            this_dict[key] = find_remove(this_dict[key], target_key)

    return this_dict

dict_nested_new = find_remove(nested_dict, "sub_key2a")

The trick

The trick is to find out in advance whether a target_key is among the next children (= this_dict[key] = the values of the current dict iteration) before you reach the child level recursively. Only then you can still delete a key:value pair of the child level while iterating over a dictionary. Once you have reached the same level as the key to be deleted and then try to delete it from there, you would get the error:

RuntimeError: dictionary changed size during iteration

The recursive solution makes any change only on the next values' sub-level and therefore avoids the error.

0

Today I had a similar use-case, but instead of simply materializing the keys on the dictionary at the beginning of the loop, I wanted changes to the dict to affect the iteration of the dict, which was an ordered dict.

I ended up building the following routine, which can also be found in jaraco.itertools:

def _mutable_iter(dict):
    """
    Iterate over items in the dict, yielding the first one, but allowing
    it to be mutated during the process.
    >>> d = dict(a=1)
    >>> it = _mutable_iter(d)
    >>> next(it)
    ('a', 1)
    >>> d
    {}
    >>> d.update(b=2)
    >>> list(it)
    [('b', 2)]
    """
    while dict:
        prev_key = next(iter(dict))
        yield prev_key, dict.pop(prev_key)

The docstring illustrates the usage. This function could be used in place of d.iteritems() above to have the desired effect.

Jason R. Coombs
  • 39,015
  • 10
  • 79
  • 86