130

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?


See also How to avoid "RuntimeError: dictionary changed size during iteration" error? for the separate question of how to avoid the problem.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
NPE
  • 486,780
  • 108
  • 951
  • 1,012
  • 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) – questionto42 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. – questionto42 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"). – questionto42 Aug 31 '21 at 08:58

9 Answers9

88

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.

Zac B
  • 3,796
  • 3
  • 35
  • 52
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • 4
    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
  • 3
    @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
  • 1
    You have to use deep copy, not just copy, `import copy copy.deepcopy(this_dict)`, see the [other answer](https://stackoverflow.com/a/69163134/11154841). Just `d.copy().items()` is not enough to avoid update problems on the run. – questionto42 Jul 08 '22 at 18:20
  • @questionto42 deepcopy is not at all necessary for the problem as asked. There is no need to copy the values at all if you are only iterating over the keys. I also would avoid copy.copy/deepcopy in general; copying in a reference-based language is something that does not have a one-size-fits-all solution, and usually you should avoid depending on whether objects have distinct ids anyway (and limit mutation as much as possible). – Ryan McCampbell Apr 04 '23 at 03:15
  • @RyanMcCampbell The question is about modifying while iterating. There is no other way than with a deep copy inside recursive loops to get this done. That is why I even tried to make this whole quesiton a duplicate and link to [How can I remove a key:value pair wherever the chosen key occurs in a deeply nested dictionary?](https://stackoverflow.com/questions/68508611/how-can-i-remove-a-keyvalue-pair-wherever-the-chosen-key-occurs-in-a-deeply-nes), which was refused, but I still think it is wrongly refused. I also think that deep copy should be avoided, but it is needed here. – questionto42 Apr 05 '23 at 13:50
  • @questionto42 if you are modifying nested dicts in nested loops then you can just copy each key set as needed in each loop. Deep copy is overkill. Or better yet collect the keys to delete and delete them in a separate loop. But this is an obscure case anyway that isn't in the question. – Ryan McCampbell Apr 07 '23 at 01:51
  • @RyanMcCampbell As far as I remember, deep copy was needed to avoid `RuntimeError: dictionary changed size during iteration`, see [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), if you modify while looping. Which is the question. If you make a copy by assignments, the new dict will be linked to the old one, and it is also not guaranteed that all pairs are kept the same way, could not find the link for that anymore. The copy module is at https://docs.python.org/3/library/copy.html. – questionto42 Apr 11 '23 at 19:02
  • A hint that a mere iteration or loop that you take to reassign a new nested dict can be risky without deep copy is the highly voted comment below the answer on this very page at https://stackoverflow.com/a/6777832/11154841. You will run into this only at large dictionaries, and then, it is often even worse since you do not check. If you know an answer to the question, you are the first one, since there is no working answer to the question up to now (my recursive deep copy trick is not an answer to the question either, but it is at least a new solution to the problem). – questionto42 Apr 11 '23 at 19:14
  • @questionto42 the "dictionary changed size during iteration" means that you're directly iterating over the dictionary itself while you're changing it. If you only iterate over a shallow copy of the keys or items you will not get an error because you are not iterating over the dictionary. Now if you want to avoid other problems due to mutability, then better yet use a dict comprehension and don't modify the dict at all: `{k: f(v) for k, v in d.items() if cond(k, v)}` – Ryan McCampbell Apr 13 '23 at 01:00
  • @RyanMcCampbell The question is about changing the dictionary while iterating over it, see its code example. My solution is about answering this question by introducing recursion and a nested change in a deep copy so that this error is suppressed. How would you run the `del` command in your loop? Since that is what the question is about. It is clear that the code in question is outdated, you would take a loop over `d.items()` now. Since your loop can drop keys or values by condition, your answer is good to go as a workaround, like mine, and will help some coders to get things done. – questionto42 Apr 19 '23 at 16:47
  • @questionto42 "How would you run del" if you can't/don't want to use a dict comprehension: ``` d = {1:'a',2:'b',3:'c',4:'d'} for k in list(d.keys()): if k % 2 == 1: del d[k] ``` No errors :) It doesn't matter if the dict is nested or has mutable values or whatever, those are unrelated to the error. – Ryan McCampbell Apr 21 '23 at 04:29
  • @RyanMcCampbell You can `del` the leaves in a non-nested dict, but you cannot just `del` the leaves in a nested dict. I once tried it your way and ran into the said error message. It is long ago now, I might be wrong, but I know that I once tried it for hours (?), to no avail. Then someone helped me with the recursion trick that lets you `del` the leaves before entering the leaf level, that was my workaround in the end, see the Q/A that I linked. If you have a good example of a nested dict with such a dict comprehension and a working `del`, please answer, you have my upvote for sure. – questionto42 Apr 25 '23 at 17:18
  • @questionto42 I don't know how to paste a proper code block in a comment, but nesting is no different, you just need to make sure to copy each key/item set before you loop over it. Or, if you want to use a comprehension: `{k1: {k2: v2 for k2, v2 in d2.items() if cond(k2, v2)} for k1, d2 in d1.items()}` (this keeps all nested items that satisfy cond.) – Ryan McCampbell Apr 27 '23 at 07:34
  • If that is true, make an answer. I answered at [How can I remove a key:value pair wherever the chosen key occurs in a deeply nested dictionary?](https://stackoverflow.com/a/68508612/11154841), with a recursion and a check on the next child level, taking the trick from [How can I replace a key:value pair by its value wherever the chosen key occurs in a deeply nested dictionary?](https://stackoverflow.com/questions/68470098/how-can-i-replace-a-keyvalue-pair-by-its-value-whereever-the-chosen-key-occurs/68471198#68471198). Show how to do it without a recursion. – questionto42 Apr 27 '23 at 15:50
64

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
  • 41,871
  • 30
  • 130
  • 181
Raphaël Saint-Pierre
  • 2,498
  • 1
  • 19
  • 23
  • 73
    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
36

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
  • 19,007
  • 10
  • 60
  • 95
  • 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
  • You have to use deep copy, not just copy, `import copy copy.deepcopy(this_dict)`, see the [other answer](https://stackoverflow.com/a/69163134/11154841). Just `d.copy().items()` is not enough to avoid update problems on the run. – questionto42 Jul 08 '22 at 18:21
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
  • 983
  • 9
  • 12
  • 1
    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
  • @philippjfr Your comment is not against this solution here, though, you only say that the mere `dict.copy().keys()` would not solve it either, but you would need a deep copy instead. That stands only against the first sentence, not against the solution. I guess that is not clear to everyone reading it. Making a list and then making a dictionary from it seems to work. Only loops over dictionaries are not good since you cannot change a dictionary while looping over it without the risk of losing data during updates. – questionto42 Jul 08 '22 at 18:16
  • If you wanted to use a deep copy, not just copy, use `import copy copy.deepcopy(this_dict)`, see the [other answer](https://stackoverflow.com/a/69163134/11154841). Just `d.copy().items()` is not enough to avoid update problems on the run. – questionto42 Jul 08 '22 at 18:23
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
  • 765
  • 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
  • 6,170
  • 18
  • 74
  • 101
2

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.

questionto42
  • 7,175
  • 4
  • 57
  • 90
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
  • 25,496
  • 15
  • 91
  • 179
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
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
  • 41,115
  • 10
  • 83
  • 93