Code review example: Deleting from a dict while iterating it raises RuntimeError in Python

Retired from the DiffDojo practice pool in October 2026; the review below is the canonical answer. An AI wrote this pull request for the task underneath. Read the diff and decide what you would say before you scroll to the review.

Python · Edge cases & error handling · intermediate

The task

Evict expired cache entries

  1. evict_expired(cache, now) removes every entry whose expiry is at or before now.
  2. cache maps key -> (value, expires_at).
  3. Entries that have not expired must remain.

Adds cache eviction: iterates the cache and deletes any entry that has expired. Removes the stale keys in one pass.

The diff

app/cache/evict.py+4
1+ def evict_expired(cache: dict, now: float) -> None:
2+ for key, (value, expires_at) in cache.items():
3+ if expires_at <= now:
4+ del cache[key]

The review

Issue 1: Deleting from a dict while iterating it raises RuntimeError

app/cache/evict.py, lines 2, 3, 4

  1. You can't resize a dict mid-iteration. del cache[key] inside for ... in cache.items() changes the dict's size, and Python raises RuntimeError: dictionary changed size during iteration on the very next loop step.
  2. It fails exactly when it has work to do. With nothing expired, the loop never deletes and completes fine, so a quick test with fresh entries passes. The first real expired entry crashes it.
  3. The bug is iterating and mutating the same object. The loop needs a view that won't change underneath it.

The fix: iterate over a snapshot, for key, (_, expires_at) in list(cache.items()):, or collect the expired keys first and delete them in a second pass.

The fix

def evict_expired(cache: dict, now: float) -> None:
    expired = [
        key for key, (_, expires_at) in cache.items() if expires_at <= now
    ]
    for key in expired:
        del cache[key]

Why it matters

Deleting or adding dict/set keys inside a loop over the same collection → RuntimeError. Snapshot with list(...) or collect-then-mutate.

Real-world stakes: the eviction job runs fine in tests with unexpired fixtures and throws in production the moment a real entry ages out, so the cache never gets cleaned and grows unbounded, or the request that triggered eviction 500s. Mutating a container mid-iteration is a classic Python edge-case bug, and assistants write it because the single-pass del inside the loop reads like the most direct thing.

Read more: python dictionary changed size during iteration, delete dict key while iterating, modify dict during loop python

The verdict

Request changes. Deleting from a dict while iterating it raises RuntimeError

Try a live one

Python PRs you have not seen the answer to.

Leave inline comments and a verdict, then see what you caught. Free, no signup.

Review a Python PR →

How to review Python code · All code review examples