Repository navigation
Logging capture crashes when another thread registers a logger #15237
Description
- a detailed description of the bug or problem you are having
- output of
pip listfrom the virtual environment you are using - pytest and operating system versions
- minimal example if possible
Description
catching_logs.__enter__ can raise RuntimeError: dictionary changed size during iteration when another thread calls logging.getLogger() during capture setup.
The regression was introduced by #14375 / 7e6161c68579eb58ecceaf8c6b6f6b1b2f46c28a, which iterates the live logging.root.manager.loggerDict.values() view. logging.getLogger() mutates that dictionary while holding logging's registry lock, but pytest's iteration does not use the lock or take a snapshot.
Reproduction
import logging
import threading
from _pytest.logging import catching_logs
mutation_completed = threading.Event()
worker = None
class TriggerLogger(logging.Logger):
@property
def propagate(self):
global worker
def register_logger():
logging.getLogger("repro.concurrent-registration")
mutation_completed.set()
worker = threading.Thread(target=register_logger)
worker.start()
assert mutation_completed.wait(timeout=1)
return True
@propagate.setter
def propagate(self, value):
pass
manager = logging.root.manager
manager.loggerDict["repro.trigger"] = TriggerLogger("repro.trigger")
try:
with catching_logs(logging.NullHandler()):
pass
finally:
if worker is not None:
worker.join(timeout=1)The property access happens after pytest's live dictionary iterator has yielded its first logger. The worker then uses the public logging.getLogger() API to mutate the registry before pytest requests the iterator's next value.
Actual result
File ".../_pytest/logging.py", line 366, in __enter__
for logger in root_logger.manager.loggerDict.values():
RuntimeError: dictionary changed size during iteration
Expected result
Concurrent logger registration should not make capture setup fail.
Suggested fix
Snapshot the registry before inspecting the loggers:
for logger in list(root_logger.manager.loggerDict.values()):This also matches the behavior described in #14375: loggers created after capture setup starts do not need to be discovered by that capture scope.
Environment
- macOS 26.5.2 (Apple silicon)
- Python 3.13.13
- pytest 9.2.0.dev393+gc9bf1edf9 (
mainatc9bf1edf9e0d388f3e7bebc88576e608ffbc40a4) - Also reproduced with pytest 9.1.1 on Python 3.12
pip list
Package Version
------------------ ----------------------------
argcomplete 3.6.3
attrs 25.4.0
certifi 2026.1.4
charset-normalizer 3.4.4
coverage 7.13.1
elementpath 5.1.0
execnet 2.1.2
hypothesis 6.150.0
idna 3.11
iniconfig 2.3.0
mock 5.2.0
numpy 2.4.1
packaging 25.0
pexpect 4.9.0
pluggy 1.6.0
ptyprocess 0.7.0
Pygments 2.19.2
pytest 9.2.0.dev393+gc9bf1edf9
pytest-xdist 3.8.0
PyYAML 6.0.3
requests 2.32.5
setuptools 80.9.0
sortedcontainers 2.4.0
urllib3 2.6.3
xmlschema 4.3.0
Activity