Python
Recursion
RuntimeError
Set
Iteration

Recursion how to avoid Python set changed set during iteration RuntimeError

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

RuntimeError: Set changed size during iteration means Python detected that the same set object was being mutated while an active iterator was walking it. Recursion makes this easier to trigger because the mutation may happen in a deeper stack frame while the outer frame still thinks it is safely iterating. The real fix is to separate iteration from mutation, not to ban recursion entirely.

Why Python Raises the Error

Sets are mutable hash tables. Their internal layout can change when items are inserted or removed, so Python does not allow the set to be structurally modified while an active iterator is walking it.

This simple example fails:

python
1values = {1, 2, 3}
2
3for item in values:
4    if item == 2:
5        values.remove(item)

It raises a runtime error because the loop and the mutation target the same set at the same time.

The same rule applies in recursive code:

python
1def visit(items):
2    for item in items:
3        if item % 2 == 0:
4            items.remove(item)
5            visit(items)

Even if the recursive call is the one doing the mutation, the outer frame is still iterating the same set object.

Safe Pattern 1: Iterate Over a Snapshot

The simplest fix is to iterate over a snapshot while mutating the original:

python
1def remove_even_numbers(items: set[int]) -> None:
2    for item in list(items):
3        if item % 2 == 0:
4            items.remove(item)
5
6values = {1, 2, 3, 4, 5}
7remove_even_numbers(values)
8print(values)  # {1, 3, 5}

list(items) captures the elements at the start of the loop, so later mutations do not affect the iterator.

For recursion:

python
1def explore(items: set[int]) -> None:
2    for item in list(items):
3        print("visiting", item)
4        items.remove(item)
5        if items:
6            explore(items)
7        break
8
9values = {1, 2, 3}
10explore(values)

This works because each frame iterates over a list copy, not over the live set object being changed.

Safe Pattern 2: Build a New Set for the Recursive Call

Sometimes mutating in place is the wrong model entirely. If recursion conceptually operates on "remaining work," create a new set and pass that down:

python
1def search(paths: set[str]) -> None:
2    if not paths:
3        return
4
5    current = next(iter(paths))
6    rest = paths - {current}
7
8    print("processing", current)
9    search(rest)
10
11search({"a", "b", "c"})

This approach is often clearer because each recursive call gets its own independent view of the remaining items.

Safe Pattern 3: Collect Changes and Apply Them Later

If the loop is doing analysis and the mutation is just a final cleanup step, collect the changes first:

python
1def prune(items: set[int]) -> None:
2    to_remove = []
3
4    for item in items:
5        if item < 0:
6            to_remove.append(item)
7
8    for item in to_remove:
9        items.remove(item)
10
11values = {-2, -1, 3, 4}
12prune(values)
13print(values)  # {3, 4}

This is often the best option when the recursive logic depends on seeing the original collection consistently during one full pass.

When pop() Is Better Than Iteration

If the algorithm is really "consume one item until nothing remains," a while loop with pop() is often simpler than iterating and mutating in the same structure:

python
1def consume(items: set[int]) -> None:
2    while items:
3        item = items.pop()
4        print("handling", item)
5
6values = {1, 2, 3}
7consume(values)

This works because there is no active iterator over the set. You are explicitly removing one element at a time.

If you still want recursion, combine pop() with a base case:

python
1def consume_recursive(items: set[int]) -> None:
2    if not items:
3        return
4
5    item = items.pop()
6    print("handling", item)
7    consume_recursive(items)

That pattern is safe because it does not iterate with for item in items.

Common Pitfalls

  • Copying the set after iteration already began. Fix: create the snapshot before the for loop starts.
  • Mutating the same set through a different variable reference. Fix: remember that object identity matters, not the variable name.
  • Using recursion where an iterative consume loop is clearer. Fix: prefer while items: plus pop() when the task is simple consumption.
  • Forgetting recursion depth limits. Fix: use iteration for large workloads where recursion could become deep.
  • Mixing analysis and mutation in the same pass. Fix: collect changes first, then apply them afterward.

Summary

  • The error happens when a set is mutated while an active iterator is using it.
  • Recursive calls do not make that rule disappear; they often make it easier to violate.
  • Safe fixes include iterating over a snapshot, passing a new set to recursive calls, or collecting changes for later.
  • If you are consuming the set one item at a time, pop() plus a while loop or a recursive base case is often the cleanest pattern.
  • When in doubt, separate iteration from mutation.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.