https://github.com/python/cpython/commit/26696a65f9f74d47050ff460bc16587632cac195
commit: 26696a65f9f74d47050ff460bc16587632cac195
branch: main
author: esadomer <[email protected]>
committer: vsajip <[email protected]>
date: 2026-05-29T16:50:05+01:00
summary:

gh-132372: Speed up logging.config existing logger handling (GH-150242)

Co-authored-by: Pieter Eendebak <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-05-22-15-30-00.gh-issue-132372.YP4a6x.rst
M Lib/logging/config.py
M Lib/test/test_logging.py

diff --git a/Lib/logging/config.py b/Lib/logging/config.py
index 9a8b7016886eeeb..e39dac432f6ab5c 100644
--- a/Lib/logging/config.py
+++ b/Lib/logging/config.py
@@ -36,6 +36,7 @@
 import threading
 import traceback
 
+from bisect import bisect_left
 from socketserver import ThreadingTCPServer, StreamRequestHandler
 
 
@@ -186,9 +187,8 @@ def _handle_existing_loggers(existing, child_loggers, 
disable_existing):
     what was intended by the user. Also, allow existing loggers to NOT be
     disabled if disable_existing is false.
     """
-    root = logging.root
     for log in existing:
-        logger = root.manager.loggerDict[log]
+        logger = logging.root.manager.loggerDict[log]
         if log in child_loggers:
             if not isinstance(logger, logging.PlaceHolder):
                 logger.setLevel(logging.NOTSET)
@@ -197,6 +197,20 @@ def _handle_existing_loggers(existing, child_loggers, 
disable_existing):
         else:
             logger.disabled = disable_existing
 
+def _forget_existing_logger(name, existing, existing_set, child_loggers):
+    """Forget a configured logger and record its existing children."""
+    prefixed = name + "."
+    i = bisect_left(existing, prefixed)
+    num_existing = len(existing)
+    while i < num_existing:
+        child = existing[i]
+        if not child.startswith(prefixed):
+            break
+        if child in existing_set:
+            child_loggers[child] = None
+        i += 1
+    existing_set.remove(name)
+
 def _install_loggers(cp, handlers, disable_existing):
     """Create and install loggers"""
 
@@ -235,25 +249,18 @@ def _install_loggers(cp, handlers, disable_existing):
     #named loggers. With a sorted list it is easier
     #to find the child loggers.
     existing.sort()
+    existing_set = set(existing)
     #We'll keep the list of existing loggers
     #which are children of named loggers here...
-    child_loggers = []
+    child_loggers = {}
     #now set up the new ones...
     for log in llist:
         section = cp["logger_%s" % log]
         qn = section["qualname"]
         propagate = section.getint("propagate", fallback=1)
         logger = logging.getLogger(qn)
-        if qn in existing:
-            i = existing.index(qn) + 1 # start with the entry after qn
-            prefixed = qn + "."
-            pflen = len(prefixed)
-            num_existing = len(existing)
-            while i < num_existing:
-                if existing[i][:pflen] == prefixed:
-                    child_loggers.append(existing[i])
-                i += 1
-            existing.remove(qn)
+        if qn in existing_set:
+            _forget_existing_logger(qn, existing, existing_set, child_loggers)
         if "level" in section:
             level = section["level"]
             logger.setLevel(level)
@@ -281,6 +288,7 @@ def _install_loggers(cp, handlers, disable_existing):
     #        logger.propagate = 1
     #    elif disable_existing_loggers:
     #        logger.disabled = 1
+    existing = [name for name in existing if name in existing_set]
     _handle_existing_loggers(existing, child_loggers, disable_existing)
 
 
@@ -638,22 +646,16 @@ def configure(self):
                 #named loggers. With a sorted list it is easier
                 #to find the child loggers.
                 existing.sort()
+                existing_set = set(existing)
                 #We'll keep the list of existing loggers
                 #which are children of named loggers here...
-                child_loggers = []
+                child_loggers = {}
                 #now set up the new ones...
                 loggers = config.get('loggers', EMPTY_DICT)
                 for name in loggers:
-                    if name in existing:
-                        i = existing.index(name) + 1 # look after name
-                        prefixed = name + "."
-                        pflen = len(prefixed)
-                        num_existing = len(existing)
-                        while i < num_existing:
-                            if existing[i][:pflen] == prefixed:
-                                child_loggers.append(existing[i])
-                            i += 1
-                        existing.remove(name)
+                    if name in existing_set:
+                        _forget_existing_logger(name, existing, existing_set,
+                                                child_loggers)
                     try:
                         self.configure_logger(name, loggers[name])
                     except Exception as e:
@@ -673,6 +675,7 @@ def configure(self):
                 #        logger.propagate = True
                 #    elif disable_existing:
                 #        logger.disabled = True
+                existing = [name for name in existing if name in existing_set]
                 _handle_existing_loggers(existing, child_loggers,
                                          disable_existing)
 
diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py
index 2ab9e0b336c9fb5..08678119200d427 100644
--- a/Lib/test/test_logging.py
+++ b/Lib/test/test_logging.py
@@ -4173,6 +4173,30 @@ def test_90195(self):
         # Logger should be enabled, since explicitly mentioned
         self.assertFalse(logger.disabled)
 
+    def test_disable_existing_loggers_preserves_children(self):
+        parent = logging.getLogger('many')
+        child = logging.getLogger('many.child')
+        child.setLevel(logging.CRITICAL)
+        self.assertFalse(child.isEnabledFor(logging.INFO))
+        cousin = logging.getLogger('many-child')
+        for i in range(20):
+            logging.getLogger(f'many-sibling-{i}')
+
+        self.apply_config({
+            'version': 1,
+            'loggers': {
+                'many': {
+                    'level': 'INFO',
+                },
+            },
+        })
+
+        self.assertFalse(parent.disabled)
+        self.assertFalse(child.disabled)
+        self.assertEqual(child.level, logging.NOTSET)
+        self.assertTrue(child.isEnabledFor(logging.INFO))
+        self.assertTrue(cousin.disabled)
+
     def test_111615(self):
         # See gh-111615
         import_helper.import_module('_multiprocessing')  # see gh-113692
diff --git 
a/Misc/NEWS.d/next/Library/2026-05-22-15-30-00.gh-issue-132372.YP4a6x.rst 
b/Misc/NEWS.d/next/Library/2026-05-22-15-30-00.gh-issue-132372.YP4a6x.rst
new file mode 100644
index 000000000000000..bcd96e88eac1bfd
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-05-22-15-30-00.gh-issue-132372.YP4a6x.rst
@@ -0,0 +1,2 @@
+Speed up :func:`logging.config.fileConfig` and
+:func:`logging.config.dictConfig` when handling many existing loggers.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to