Почему нельзя изменять словарь во время итерации и как это обойти в Python
Что проверяют
Понимаете ли вы, как устроен итератор словаря, или помните только текст исключения. Обычно следом уточняют, чем отличается изменение значения от добавления ключа и почему у списка при том же сценарии ошибки нет.
Как отвечать
Итератор словаря ключи не копирует: он держит ссылку на сам словарь, позицию в хеш-таблице и число элементов на момент создания. На каждом шаге счётчик сверяется с текущим, и при расхождении CPython бросает RuntimeError: dictionary changed size during iteration. Причина не в «испортившейся копии»: вставка может вызвать пересборку таблицы, запомненная позиция станет бессмысленной, и часть ключей вы пропустите, а часть увидите дважды.
- что разрешено: присвоить новое значение существующему ключу можно, размер не меняется. Запрещены вставка и удаление;
- отдельный случай: если удалить один ключ и добавить другой, размер совпадёт, но с Python 3.8 это ловится отдельно и даёт
RuntimeError: dictionary keys changed during iteration; - как обходить: идти по снимку
for key in list(d)и удалять из оригинала; ещё лучше собрать новый словарь включениемd = {k: v for k, v in d.items() if k not in bad}, это и быстрее, и очевиднее читателю; - разобрать целиком:
while d: key, value = d.popitem(), тогда итератора просто нет; - соседние структуры: у множества поведение то же (
Set changed size during iteration). У списка исключения нет вовсе, и удаление по ходу цикла молча пропускает элементы, что хуже: ошибка не видна. Фильтруйте его включением или заменой срезаlst[:] = [...].
То же ограничение действует между потоками: словарь, который правит соседний поток, уронит ваш цикл. В свободнопоточной сборке Python 3.14 это куда вероятнее, чем во времена глобальной блокировки интерпретатора.
Где спотыкаются
Повторяют миф «Python делает копию ключей». Копии нет, есть сверка счётчика, и от этого зависит, какие правки безопасны.
Считают запрещённым любое изменение. Присваивание в существующий ключ безопасно, и часто это всё, что нужно.
Переносят привычку на список и удаляют элементы прямо в цикле for. Исключения не будет, зато часть элементов останется необработанной, и дефект всплывёт на проде.