-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Description
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
- Set up a ReadTheDocs build using Python 3.14 (or wait for RTD to auto-update to Python 3.14 as "latest")
- Build documentation that executes Mesa notebooks, such as the visualization tutorial
- 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 agentAdditional 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
WeakKeyDictionaryhas an internal_IterationGuardthat prevents removals during iteration, but only when using the dict's own iteration methods (likeitems(),keys()with proper guards) - not when accessing.keys()directly and returning it - Other
AgentSetmethods likedo()andshuffle_do()already iterate safely usingself._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