RuntimeError: dict changed size during iteration

Posted at — Jul 30, 2021

A few days ago, I was writing some code at my internship to fix an issue/add a feature to the codebase. To test if my code could run, I created a simple version that I could test in the python interpreter. Essentially, it boiled down to this:

health_checks = {}
invalid_config_options = []
warning_name = ‘1st_INVALID_CONFIG_OPTION’
health_checks[warning_name] = ‘error string here’
invalid_config_options.append(warning_name)

for warning in health_checks:
    if warning not in invalid_config_options and warning.endswith(‘INVALID_CONFIG_OPTIONS’):
        del health_checks[warning]

Because the warning is in the invalid_config_options list, which we appended to track the active errors, the last line del health_checks[warning] will not execute. Now, imagine that the error is not active anymore, i.e. invalid_config_options list is empty.

invalid_config_options = []

Then in our intrepreter, say we run this for loop again:

for warning in health_checks:
    if warning not in invalid_config_options and warning.endswith(‘INVALID_CONFIG_OPTIONS’):
        del health_checks[warning]

Then it will delete the warning from the health_checks dict, but after raise a RuntimeError: dictionary changed size during iteration. Basically, it will succeed on that specific iteration of the for loop, but will refuse to continue iterating and raise the error when it reaches the for warning in health_checks again. And the reason for failure is because you’re trying to change the entries of the dict while it’s being iterated.

To solve this, there are a few solutions. The one I think is most intuitive is this: for warning in health_checks.copy() Iterating over a shallow copy of the dict, and deleting entries in the original dict. A shallow copy is a separate object in python that shares the same reference to the original object. So if you change the original dict, the change will reflect in the shallow copy. But if you change the shallow copy dict, the change will not reflect in the original dict. So because the copy is a separate object, then you’re not iterating over the same thing that you’re changing.

Similarly with lists, if you try to add or remove elements while iterating over the list, you can accidentally skip elements because the list is going through the indices, and isn’t aware of the changing elements. e.g. if you have a list like this: l = [1, 2, 4, 6, 9] and you want to remove the even elements of the list while iterating through it like this:

for element in l:
    if element % 2 == 0:
        l.remove(element)

you will end up with l = [1, 4, 9].

With lists, you can do something similar with what was previously done with dict. You can instead iterate through a copy of the list like l.copy() or list(l). Or you could iterate through a reversed(l) list so that the removed element’s index only affects the indexes that were already handled and doesn’t affect the iteration. Or you could use list comprehension: [x for x in l if x % 2 != 0]. In the end, it probably isn’t great practice to change a mutable object while iterating it because it could have unforeseen results, but there are ways to handle it.