Skip to content

AgentSet iteration fails on Python 3.14 with "dictionary changed size during iteration" #2896

@EwoutH

Description

@EwoutH

Edit: I filled this bug wrongly. It's actually not at our end, but Solara's: widgetti/solara#1107.

Describe the bug

ReadTheDocs documentation builds are failing on Python 3.14 with RuntimeError: dictionary changed size during iteration. This is caused by Mesa's AgentSet class iterating directly over a WeakKeyDictionary without protecting against garbage collection removing entries during iteration.

The AgentSet.__iter__ method in agent.py returns self._agents.keys() directly:

def __iter__(self) -> Iterator[Agent]:
    """Provide an iterator over the agents in the AgentSet."""
    return self._agents.keys()

Since _agents is a WeakKeyDictionary, if garbage collection runs during iteration and removes a dead reference, the dictionary size changes and Python raises RuntimeError: dictionary changed size during iteration.

This issue became visible in Python 3.14 likely due to changes in garbage collection timing, free-threading improvements, or stricter iterator invalidation checks.

Expected behavior

Documentation builds (and any code iterating over AgentSet) should work reliably without raising RuntimeError due to GC-related dictionary mutations.

To Reproduce

  1. Set up a ReadTheDocs build using Python 3.14 (or wait for RTD to auto-update to Python 3.14 as "latest")
  2. Build documentation that executes Mesa notebooks, such as the visualization tutorial
  3. Observe the build failure with the stack trace ending in:
    RuntimeError: dictionary changed size during iteration
    

Full traceback from ReadTheDocs build:

RuntimeError: dictionary changed size during iteration
...
myst_nb.core.execute.base.ExecutionError: /home/docs/checkouts/readthedocs.org/user_builds/mesa/checkouts/latest/docs/tutorials/4_visualization_basic.ipynb

Proposed Fix

Change AgentSet.__iter__ to return a snapshot of the keys rather than a live iterator:

def __iter__(self) -> Iterator[Agent]:
    """Provide an iterator over the agents in the AgentSet."""
    return iter(list(self._agents.keys()))

This creates a list copy before iteration begins, which is immune to dictionary size changes during iteration. This approach is the standard solution recommended in Python's weakref documentation.

Alternatively, for better performance, iterate over the keyrefs() as is already done in methods like do() and shuffle_do():

def __iter__(self) -> Iterator[Agent]:
    """Provide an iterator over the agents in the AgentSet."""
    for ref in self._agents.keyrefs():
        if (agent := ref()) is not None:
            yield agent

Additional context

  • Python 3.14 was released on October 7, 2025
  • Python 3.14 includes significant free-threading improvements (PEP 703) which may have changed GC timing
  • The WeakKeyDictionary has an internal _IterationGuard that prevents removals during iteration, but only when using the dict's own iteration methods (like items(), keys() with proper guards) - not when accessing .keys() directly and returning it
  • Other AgentSet methods like do() and shuffle_do() already iterate safely using self._agents.keyrefs() with explicit null checks
  • Similar issues have been reported historically: "RuntimeError: Dictionary changed size during iteration" when copying a WeakValueDictionary python/cpython#79796

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugRelease notes label

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions