Skip to content

Logging capture crashes when another thread registers a logger #15237

Open

Description

@Spikhalskiy
  • a detailed description of the bug or problem you are having
  • output of pip list from 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 (main at c9bf1edf9e0d388f3e7bebc88576e608ffbc40a4)
  • 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions