Fix: Python RuntimeError: dictionary changed size during iteration
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 iterationOr in older Python versions:
RuntimeError: dictionary changed size during iterationYou 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 keysModifying 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 tolist(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 iterationCommon 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 errorFix 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 iterationFixed:
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
passOr 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: AWS Lambda Unable to import module / Runtime.ImportModuleError
How to fix the AWS Lambda Runtime.ImportModuleError and Unable to import module error caused by wrong handler paths, missing dependencies, layer issues, and packaging problems.
Fix: Python TypeError: unhashable type: 'list'
Learn why Python raises TypeError unhashable type list, dict, or set and how to fix it when using dictionary keys, sets, groupby, dataclasses, and custom classes.
Fix: Django Forbidden (403) CSRF verification failed
How to fix Django 403 CSRF verification failed error caused by missing CSRF tokens, AJAX requests, cross-origin issues, HTTPS misconfig, and session problems.
Fix: FastAPI 422 Unprocessable Entity (validation error)
How to fix FastAPI 422 Unprocessable Entity error caused by wrong request body format, missing fields, type mismatches, query parameter errors, and Pydantic validation.