エラーの内容
辞書をループしながら、同じループ内でキーを削除または追加しています。Pythonは直ちに以下のエラーを投げます:
RuntimeError: dictionary changed size during iteration
典型的な例として、偶数の値を持つキーを削除してフィルタリングする場合を見てみましょう:
data = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
for key in data:
if data[key] % 2 == 0:
del data[key] # ここでRuntimeError
ループの途中でキーを追加しても同じエラーが発生します:
cache = {'x': 10}
for key in cache:
cache[key + '_copy'] = cache[key] # RuntimeError: dictionary changed size during iteration
なぜこのエラーが起きるのか
Pythonのfor key in dictは、辞書のサイズを監視する内部イテレータを生成します。キーを追加または削除するとサイズが変わります。Pythonはこれを即座に検出し、意図的にRuntimeErrorを発生させます。
この保護機能がなければ、イテレータがキーをスキップしたり、同じキーを二度訪問したり、予測不可能なクラッシュを引き起こす可能性があります。Python 3ではこれをハードエラーとしています。Python 2では暗黙的に許容していましたが、それがデバッグほぼ不可能なデータ破損を招いていました。
**重要な区別:**既存のキーの値を変更することは全く問題ありません。キーの追加や削除(辞書のサイズが変わる操作)のみがこのエラーを引き起こします。
手軽な修正:コピーに対してイテレートする
ループの対象をlist()で包みましょう。イテレーション開始前にキーを通常のリストにスナップショットするため、その下で辞書が自由に変更できます。
data = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
for key in list(data.keys()): # list()でスナップショットを作成
if data[key] % 2 == 0:
del data[key]
print(data) # {'a': 1, 'c': 3}
キーと値の両方が必要な場合も、.items()で同じテクニックが使えます:
for key, value in list(data.items()):
if value % 2 == 0:
del data[key]
一行の変更で、新しい概念もゼロ。既存コードへのクイックパッチに最適です。
よりクリーンな修正:辞書内包表記
フィルタリングの用途であれば、ミューテーションを完全に避けて新しい辞書を作成しましょう:
data = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
data = {k: v for k, v in data.items() if v % 2 != 0}
print(data) # {'a': 1, 'c': 3}
インプレースのミューテーションがないため、エラーのリスクもありません。条件が式の中に直接書かれているため、読みやすさも向上します。これがPython 3における辞書フィルタリングの推奨アプローチです。
イテレーション中にキーを追加する場合の修正
既存のエントリを処理しながら新しいエントリを生成する必要がある場合は、別の辞書に収集してからループ後にマージしましょう。
cache = {'x': 10, 'y': 20}
# 誤り:ループ内でcacheに追加
# for key in cache:
# cache[key + '_copy'] = cache[key] # RuntimeError
# 正解:新しいエントリを別に保管する
new_entries = {}
for key, value in cache.items():
new_entries[key + '_copy'] = value
cache.update(new_entries)
print(cache) # {'x': 10, 'y': 20, 'x_copy': 10, 'y_copy': 20}
ループはcacheからの読み取りのみを行います。new_entriesがすべての書き込みを受け取ります。そして最後に一度のupdate()で安全にすべてをマージします。
キーのリネームの修正
キーのリネームはより複雑です — 追加と削除の両方が発生します。収集してから適用するパターンを使いましょう:
config = {'debug_mode': True, 'debug_level': 3, 'timeout': 30}
# 'debug_'で始まるすべてのキーを'log_'にリネーム
to_rename = {k: k.replace('debug_', 'log_') for k in config if k.startswith('debug_')}
for old_key, new_key in to_rename.items():
config[new_key] = config.pop(old_key)
print(config) # {'timeout': 30, 'log_mode': True, 'log_level': 3}
第一パス:リネーム対象を特定する(ミューテーションなし)。第二パス:リネームを適用する。明確な分離により、イテレータの競合が発生しません。
ネストされた辞書
外側の辞書をイテレートしながら内側の辞書を変更することは問題ありません — 外側のイテレータは気にしません。ただし外側の辞書自体のサイズが変わると、通常通りエラーが発生します:
users = {
'alice': {'score': 10, 'active': True},
'bob': {'score': 5, 'active': False},
'charlie': {'score': 8, 'active': True},
}
# 非アクティブユーザーを削除 — 外側のキーを先にコピーするので安全
for name in list(users):
if not users[name]['active']:
del users[name]
print(users) # {'alice': {...}, 'charlie': {...}}
確認
修正を適用した後、コードを実行して結果が期待通りであることを確認しましょう:
data = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
data = {k: v for k, v in data.items() if v % 2 != 0}
assert data == {'a': 1, 'c': 3}, f"Unexpected result: {data}"
print("OK:", data)
期待される出力:
OK: {'a': 1, 'c': 3}
RuntimeErrorなし、正しいキーと値 — これで完了です。
まとめ
- 辞書を直接イテレートしながらキーを追加・削除しないでください。
- **手軽な修正:**ループの対象を
list()で包む — 例:for key in list(data)。 - **フィルタリングの推奨修正:**既存の辞書をミューテートせず、辞書内包表記で新しい辞書を生成する。
- **キーの追加の場合:**新しいエントリを別の辞書に収集し、ループ後に
.update()を呼び出す。 - **キーのリネームの場合:**まずリネームマップを作成し、第二パスで適用する。

