Skip to content

gh-156777: Optimize import overhead in the logging package - #156778

Open
brittanyrey wants to merge 9 commits into
python:mainfrom
brittanyrey:b-perf-logging-lazy-imports
Open

brittanyrey wants to merge 9 commits into
python:mainfrom
brittanyrey:b-perf-logging-lazy-imports

Conversation

@brittanyrey

@brittanyrey brittanyrey commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

summary

  • Lift the function-level imports in logging/handlers.py and logging/config.py to module scope as lazy imports, and mark other cold-path imports lazy.
  • Imports reachable from interpreter finalization stay eager and are now commented as such.
  • The Windows-only (_winapi, winreg) and optional third-party (win32evtlogutil) imports stay function-level on purpose: as module-level lazy imports they would break inspect.getmembers() and pydoc on non-Windows.

perf

Import before after delta
import logging.config 24.91 ms 18.11 ms −27.3%
import logging.handlers 21.97 ms 17.79 ms −19.0%
import logging 17.10 ms 17.11 ms neutral

Lift the function-level imports in logging/handlers.py and logging/config.py
to module scope as lazy imports, and mark other cold-path imports lazy.

Imports reachable from interpreter finalization stay eager and are now
commented as such: at teardown sys.modules has been cleared, so resolving a
lazy import raises ImportError('sys.meta_path is None').

The Windows-only (_winapi, winreg) and optional third-party
(win32evtlogutil) imports stay function-level on purpose: as module-level
lazy imports they would break inspect.getmembers() and pydoc on non-Windows.

import logging.config    -28.3%
import logging.handlers  -17.8%
import logging           unchanged
Call-time benchmarks unchanged (all within noise).
Comment thread Lib/logging/__init__.py Outdated
Comment thread Lib/logging/handlers.py Outdated
Comment thread Misc/NEWS.d/next/Library/2026-08-31-08-20-00.gh-issue-156777.La7yLg.rst Outdated
Co-authored-by: Pieter Eendebak <pieter.eendebak@gmail.com>
Comment thread Lib/logging/config.py Outdated
Comment thread Lib/logging/config.py Outdated
Comment thread Lib/logging/handlers.py
Comment thread Lib/logging/__init__.py Outdated
Sort each import block into the isort/Ruff groups requested in review:
plain imports, from-imports, lazy imports, lazy from-imports.

In logging/config.py, replace ``lazy import logging.handlers`` with
``lazy from logging import handlers``. The former binds the name
``logging`` lazily, so it shadows the eager ``import logging`` above it
and any use of ``logging.<anything>`` in the module pulls in
logging.handlers. Aliased to ``logging_handlers`` because ``handlers``
is already a local variable in _install_handlers() and
_configure_queue_handler().

Now logging.handlers is imported only when a handler is configured
through the ``class`` key; previously dictConfig(), fileConfig() and
stopListening() all reified it.

Co-authored-by: Pieter Eendebak <pieter.eendebak@gmail.com>
Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
Comment thread Lib/logging/config.py

@pablogsal pablogsal left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found three small things, see the inline comments.

Comment thread Lib/logging/config.py Outdated
Comment thread Lib/logging/handlers.py
Comment thread Lib/logging/handlers.py Outdated
lazy import queue
lazy import smtplib
lazy import socket
lazy import ssl

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should ssl stay inside the TLS branch too? Moving it here breaks inspect.getmembers(logging.handlers) on builds without _ssl, for the same reason as the optional Windows imports.

@bedevere-app

bedevere-app Bot commented Sep 18, 2026

Copy link
Copy Markdown

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

sprajs

This comment was marked as spam.

@pablogsal

Copy link
Copy Markdown
Member

@sprajs, thanks for wanting to help. This review largely repeats findings already reported, which adds to the maintainers’ reading workload without helping move the PR forward.

Please read CPython’s guidelines for using AI tools, particularly the expectations around judgment and productive contributions. Please stop posting AI-assisted reviews that restate existing feedback. We’d appreciate keeping the discussion focused on new, actionable information.

Keep ssl and multiprocessing.queues as function-level imports. A
module-level lazy import is resolved by inspect.getmembers() and pydoc,
so ssl breaks them on a build without _ssl, and multiprocessing defeats
the deferral the comment there asks for. Neither costs anything: a
nested import and a module-level lazy import defer identically.

Leave pickle, socket and struct eager in handlers.py. A record emitted
from a __del__ during finalization can no longer import, so a
SocketHandler pickled its first record too late and dropped it.

Resolve logging.handlers in fileConfig() before it evaluates any config
expression against vars(logging), so the documented handlers.X spelling
works again in a fresh process, both in class= and in defaults=.

Add tests for each, and pin the laziness that is left.
@brittanyrey
brittanyrey marked this pull request as draft September 23, 2026 17:57
pickle, socket and struct go back to lazy. emit() can run during
finalization, when importing no longer works, so SocketHandler.__init__
resolves them while it still can. Keeping them eager instead gives back
three quarters of the handlers.py import-time win.
Reverts the lazy alias and the nine logging_handlers.* renames that came
with it. The alias left "handlers" out of vars(logging), which broke the
documented handlers.X spelling in fileConfig() config files, and it was
worth only 0.68 ms of the 6.1 ms win on importing logging.config.

Also drop the two comments that no longer carry their weight, so the io
and ssl hunks disappear from the diff, and trim the new tests.
Drops the "as logging_handlers" alias and the nine renames that came with
it. The dotted spelling defers the submodule just as well for import time,
and resolving the module's own logging global is what binds handlers in
vars(logging) again, so the documented handlers.X spelling in fileConfig()
config files works in a fresh process.
@brittanyrey
brittanyrey marked this pull request as ready for review September 23, 2026 18:29
@brittanyrey

Copy link
Copy Markdown
Contributor Author

Addressed comments, added new tests to repro the corner cases @pablogsal pointed out, and cleaned up come of the churn that had slipped in to the changes.

@hugovk

hugovk commented Sep 24, 2026

Copy link
Copy Markdown
Member

(I merged in main to fix the unrelated docs failure: KeyError: 'soft-deprecated'.)

Comment thread Lib/logging/config.py
from socketserver import ThreadingTCPServer, StreamRequestHandler
lazy import configparser
lazy import json
lazy import logging.handlers

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this also rebinds logging in this module to a lazy object, and that is what makes class=handlers.X work in fileConfig() (the logging._lock access reifies it before the eval). Could we add a small comment here explaining this? It is very easy to break by turning it back into lazy from logging import handlers.

Comment thread Lib/logging/handlers.py
import threading
import time
lazy import base64
lazy import copy

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same problem as with SocketHandler: QueueHandler.prepare() calls copy.copy(), so a QueueHandler emitting its first record from a __del__ at shutdown now cannot import copy and drops the record. copy was eager before, so I think we should keep it eager (or resolve it in QueueHandler.__init__ like you did for SocketHandler). Can we add a shutdown test for this one too?

@pablogsal

Copy link
Copy Markdown
Member

Thanks! I checked the three fixes and the new tests. They look good to me.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants