Skip to content

Fix: Python RuntimeError: dictionary changed size during iteration

FixDevs ·

Quick Answer

How to fix Python RuntimeError dictionary changed size during iteration caused by modifying a dict while looping over it, with solutions using copies, comprehensions, and safe patterns.

The Error

You run Python code and get:

RuntimeError: dictionary changed size during iteration

Or in older Python versions:

RuntimeError: dictionary changed size during iteration

You modified a dictionary (added or removed keys) while iterating over it with a for loop. Python detects this and raises a RuntimeError to prevent undefined behavior.

Why This Happens

When you iterate over a dictionary with for key in my_dict, Python creates an internal iterator that tracks the dictionary’s state. If the dictionary’s size changes (keys added or removed) during iteration, the iterator detects the inconsistency and raises RuntimeError.

This is a safety mechanism. In languages without this protection, modifying a collection during iteration leads to skipped items, infinite loops, or crashes.

This triggers the error:

my_dict = {"a": 1, "b": 2, "c": 3}

for key in my_dict:
    if my_dict[key] < 2:
        del my_dict[key]  # RuntimeError!

This does NOT trigger the error:

my_dict = {"a": 1, "b": 2, "c": 3}

for key in my_dict:
    my_dict[key] = my_dict[key] * 2  # OK — modifying values, not adding/removing keys

Modifying values is fine. Adding or removing keys is not.

Common causes:

  • Deleting keys during iteration. Removing entries that do not meet a condition.
  • Adding keys during iteration. Inserting new entries based on existing ones.
  • Indirect modification. A function called inside the loop modifies the same dictionary.
  • Multi-threaded access. Another thread modifies the dictionary while the current thread iterates.

Fix 1: Iterate Over a Copy of the Keys

Create a copy of the keys before iterating:

Broken:

users = {"alice": 1, "bob": 5, "charlie": 0, "dave": 3}

for user in users:
    if users[user] == 0:
        del users[user]  # RuntimeError!

Fixed — copy keys with list():

users = {"alice": 1, "bob": 5, "charlie": 0, "dave": 3}

for user in list(users):  # list() creates a snapshot of the keys
    if users[user] == 0:
        del users[user]  # Safe — iterating over the copy

print(users)  # {"alice": 1, "bob": 5, "dave": 3}

list(users) creates a list of keys at that point in time. The for loop iterates over this static list, not the live dictionary, so modifications are safe.

Also works with .keys(), .values(), .items():

for key, value in list(users.items()):
    if value == 0:
        del users[key]

Pro Tip: list(my_dict) is equivalent to list(my_dict.keys()). Both create a snapshot of the keys. The first form is shorter and slightly faster.

Fix 2: Use Dictionary Comprehension

Build a new dictionary instead of modifying the existing one:

users = {"alice": 1, "bob": 5, "charlie": 0, "dave": 3}

# Keep only users with non-zero values
users = {user: score for user, score in users.items() if score != 0}

print(users)  # {"alice": 1, "bob": 5, "dave": 3}

This is the most Pythonic approach for filtering dictionaries. It creates a new dictionary and reassigns the variable.

For transforming values:

prices = {"apple": 1.0, "banana": 0.5, "cherry": 2.0}

# Apply 10% discount to all prices
prices = {item: price * 0.9 for item, price in prices.items()}

For conditional transformation:

data = {"a": 1, "b": -2, "c": 3, "d": -4}

# Keep positives, double their value
data = {k: v * 2 for k, v in data.items() if v > 0}
# {"a": 2, "c": 6}

Fix 3: Collect Keys to Delete, Then Delete

Separate the “find” phase from the “modify” phase:

config = {"debug": True, "verbose": True, "timeout": 30, "temp_file": "/tmp/x"}

# Phase 1: Collect keys to remove
keys_to_remove = [key for key in config if key.startswith("temp_")]

# Phase 2: Remove them
for key in keys_to_remove:
    del config[key]

This pattern is especially useful when the deletion logic is complex or involves multiple conditions:

inventory = {"widget_a": 0, "widget_b": 15, "widget_c": 0, "widget_d": 8}

# Find all out-of-stock items
out_of_stock = [item for item, count in inventory.items() if count == 0]

# Remove them
for item in out_of_stock:
    del inventory[item]

print(inventory)  # {"widget_b": 15, "widget_d": 8}

Fix 4: Fix Indirect Modifications

A function called inside the loop might modify the dictionary:

Broken:

def process_item(data, key):
    # This function adds new keys to the same dict!
    if data[key] > 10:
        data[f"{key}_processed"] = True  # Modifies the dict being iterated!

items = {"a": 5, "b": 15, "c": 20}

for key in items:
    process_item(items, key)  # RuntimeError!

Fixed — use a copy or collect modifications:

def process_item(data, key):
    if data[key] > 10:
        return {f"{key}_processed": True}
    return {}

items = {"a": 5, "b": 15, "c": 20}
updates = {}

for key in list(items):
    updates.update(process_item(items, key))

items.update(updates)  # Apply all modifications after iteration

Common Mistake: Not realizing that a function modifies the dictionary. When debugging this error, check every function called inside the loop to see if any of them add or remove keys from the dictionary being iterated.

Fix 5: Use dict.pop() with a Separate Collection

For removing specific keys based on lookups:

cache = {"user:1": "Alice", "user:2": "Bob", "temp:1": "session", "temp:2": "data"}

# Remove all temporary entries
temp_keys = [k for k in cache if k.startswith("temp:")]
for key in temp_keys:
    cache.pop(key, None)  # pop with default avoids KeyError

print(cache)  # {"user:1": "Alice", "user:2": "Bob"}

pop() vs del:

# del raises KeyError if key doesn't exist
del my_dict["missing_key"]  # KeyError!

# pop with default returns the default value instead
my_dict.pop("missing_key", None)  # Returns None, no error

Fix 6: Fix Sets and Other Collections

The same error occurs with sets:

numbers = {1, 2, 3, 4, 5}

for n in numbers:
    if n % 2 == 0:
        numbers.discard(n)  # RuntimeError: Set changed size during iteration

Fixed:

numbers = {1, 2, 3, 4, 5}

# Set comprehension
numbers = {n for n in numbers if n % 2 != 0}

# Or iterate over a copy
for n in list(numbers):
    if n % 2 == 0:
        numbers.discard(n)

# Or use set operations
numbers -= {n for n in numbers if n % 2 == 0}

For lists, Python does not raise this error, but modifying a list during iteration causes skipped elements:

items = [1, 2, 3, 4, 5]

# This silently skips elements — no error, but wrong results!
for item in items:
    if item % 2 == 0:
        items.remove(item)

print(items)  # [1, 3, 5] — looks right but only by coincidence!

Always use list() copies or comprehensions for safe iteration.

Fix 7: Fix Multi-Threaded Dictionary Access

If multiple threads access the same dictionary, modifications in one thread can cause this error in another:

Broken:

import threading

shared_data = {"count": 0}

def writer():
    for i in range(1000):
        shared_data[f"key_{i}"] = i

def reader():
    for key in shared_data:  # RuntimeError if writer adds keys concurrently
        _ = shared_data[key]

t1 = threading.Thread(target=writer)
t2 = threading.Thread(target=reader)
t1.start()
t2.start()

Fixed — use a lock:

import threading

shared_data = {"count": 0}
lock = threading.Lock()

def writer():
    for i in range(1000):
        with lock:
            shared_data[f"key_{i}"] = i

def reader():
    with lock:
        snapshot = dict(shared_data)  # Copy under lock
    for key in snapshot:
        _ = snapshot[key]

Fixed — use a thread-safe alternative:

from collections import defaultdict
from queue import Queue

# For producer-consumer patterns, use Queue instead of shared dicts
work_queue = Queue()

Fix 8: Use defaultdict Safely

collections.defaultdict creates keys on access, which can cause the same error:

Broken:

from collections import defaultdict

counts = defaultdict(int, {"a": 1, "b": 2, "c": 3})

for key in counts:
    # Accessing a missing key with defaultdict creates it!
    if counts[key + "_total"] > 0:  # Creates "a_total", "b_total", etc.!
        pass  # RuntimeError!

Fixed — use .get() or in check:

for key in list(counts):
    if counts.get(key + "_total", 0) > 0:  # .get() doesn't create keys
        pass

Or use key in counts to check existence without creating the key.

Still Not Working?

Check for nested dictionary iteration. If you iterate over a parent dictionary and a function modifies a nested dictionary, that is fine — the error only occurs when the dictionary being iterated changes size.

Check for __del__ or __setattr__ side effects. Custom destructors or attribute setters might modify the dictionary as a side effect of other operations.

Use copy.deepcopy() for complex nested structures:

import copy

original = {"a": {"nested": [1, 2, 3]}, "b": {"nested": [4, 5, 6]}}
snapshot = copy.deepcopy(original)

for key in snapshot:
    # Modify original freely
    del original[key]

For related Python errors with list indexing, see Fix: Python IndexError: list index out of range. For TypeError issues with None values, see Fix: Python TypeError: ‘NoneType’ object is not subscriptable. For other iteration-related issues, see Fix: Python KeyError.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles