Finds which loggers logging.config.dictConfig() (or fileConfig()) is about to silently disable, before the missing log lines get noticed.
import keeploggers
names = keeploggers.would_disable(config)
if names:
print(f"this config would disable: {names}")logging.config.dictConfig() defaults disable_existing_loggers to True. Any logger created before the config call — which, for a third-party library, means essentially always, since libraries call logging.getLogger(__name__) at import time, long before an application gets around to configuring logging — comes back with .disabled = True. Its .warning() calls still work syntactically. They emit nothing. Nothing in the standard library reports that this happened.
This is current stdlib behavior, checked against Python 3.14.7's logging/config.py, _handle_existing_loggers, and its two call sites in DictConfigurator.configure and _install_loggers: a minimal config that only touches root disables every other logger that already exists, because disable_existing_loggers was never mentioned and silently defaults to True.
The usual workarounds do not hold up:
- Setting
disable_existing_loggers=Falseexplicitly works, but it is not the default, nothing warns that it matters, and most people find out by noticing a library has gone quiet. - Configuring logging before importing anything else does not hold up in a real application. Frameworks (Flask, Django, Celery) configure logging during app startup, after most of the dependency tree is already imported and has already called
getLogger. - Calling
logging.getLogger(name)again after configuring returns the sameLoggerobject. Only a placeholder ancestor gets replaced by a fresh instance on the nextgetLoggercall; an already-existing real logger keeps whateverdisable_existing_loggersdid to it.
disable_existing_loggers=True is not wrong on its own — reconfiguring logging and clearing out stale loggers on purpose is a real use case. The problem is that it is also the default for a call that most applications make exactly once, and it fires whether or not the caller meant to disable anything.
Measured: python examples/demo.py
--- 1. plain logging.config.dictConfig(), application-only config ---
before configuring: somelib_logger.disabled = False
after configuring: somelib_logger.disabled = True
calling somelib_logger.warning(...) now:
(nothing printed above this line -- the warning call was silently dropped)
--- 2. keeploggers.would_disable() -- checked before applying anything ---
would_disable(config) -> ['somelib.http']
--- 3. keeploggers.dict_config() -- applies the config, warns about it ---
DisabledLoggersWarning: dictConfig() disabled 1 existing logger(s) because 'disable_existing_loggers' was not set (it defaults to True): somelib.http. Set it explicitly, or pass on_disable='ignore', to silence this.
somelib_logger.warning(...) still emits nothing (the config did disable it --
dict_config warns about the default, it doesn't change what dictConfig does):
--- 4. keeploggers.disabled_loggers() -- the runtime diagnostic ---
disabled_loggers() -> ['somelib.http']
No existing package checks for this against the standard library's logging.config before or after applying a config; the alternatives above are the manual workarounds people use instead.
pip install keeploggersPython >= 3.11. Zero runtime dependencies — this only calls into logging and logging.config from the standard library.
import keeploggers
names = keeploggers.would_disable(config)
if names:
print(f"this config would disable: {names}")Pure and read-only: it inspects the current logger manager and mirrors the stdlib's own decision (including which loggers are spared as children of an explicitly-configured parent, and the fact that incremental: True configs never disable anything), without calling dictConfig itself.
would_disable_file(fname) does the same for a fileConfig-style .ini file, reading it with configparser the same way fileConfig does.
import keeploggers
keeploggers.dict_config(config) # on_disable="warn" by defaultIf config does not set disable_existing_loggers itself, and applying it would disable an existing logger, this warns (naming the loggers) after applying the config — the config still behaves exactly like logging.config.dictConfig(config) would; the difference is finding out about it. If config does set the key, dict_config applies it and says nothing: that was a deliberate choice, not the default catching the caller.
keeploggers.dict_config(config, on_disable="raise") # refuse to apply it at all
keeploggers.dict_config(config, on_disable="ignore") # apply it silently, like dictConfigkeeploggers.disabled_loggers()
# ['somelib.http']The "where did my logs go" diagnostic — no config dict needed, just inspects live state. Drop it in a shell, a health-check endpoint, or a breakpoint.
| Name | Type | Default | Description |
|---|---|---|---|
would_disable(config) |
function | — | Names of existing loggers dictConfig(config) would disable. Nothing applied. |
would_disable_file(fname, defaults=None, disable_existing_loggers=True, encoding=None) |
function | disable_existing_loggers=True |
Same, for a fileConfig-style .ini file. |
dict_config(config, *, on_disable="warn") |
function | on_disable="warn" |
dictConfig(config), warning/raising/ignoring per on_disable when the default silently disables loggers. Accepts "warn", "raise", or "ignore". |
disabled_loggers() |
function | — | Names of loggers that are disabled right now, in the running process. |
DisabledLoggersWarning |
exception (UserWarning) |
— | Raised as a warning by dict_config when applying a config silently disabled loggers. Carries the affected names as .names. |
DisabledLoggersError |
exception (RuntimeError) |
— | Raised by dict_config with on_disable="raise" instead of applying a config that would silently disable existing loggers. Carries the affected names as .names. |
_compute_would_disable ports the existing/child-loggers walk from logging.config._handle_existing_loggers: everything currently in the logger manager gets marked as "would disable" except loggers named directly in the incoming config and their children, which are reset rather than disabled. would_disable and would_disable_file run this walk read-only, without calling dictConfig/fileConfig. dict_config runs the same check, then calls the real dictConfig and reports on what it did per on_disable. Placeholder entries (logging.PlaceHolder, created by the manager for ancestors of a logger that was never itself created) are never reported — nothing can log through one, and the stdlib's own disabling of it has no observable effect.
- It cannot see loggers created after configuration. That is fine —
disable_existing_loggersonly ever touches loggers that already existed at the momentdictConfig/fileConfigran, so a logger created afterward was never a candidate for this in the first place. - No
fileConfigsupport for exoticConfigParserinput (a live file-like object mid-stream, customRawConfigParsersubclasses handed in pre-parsed) —would_disable_filereads a path, anos.PathLike, or a plain text file object the same wayfileConfigdoes; anything more unusual is not covered. - Does not cover third-party config systems. Django's
LOGGINGsetting, for example, is handed tologging.config.dictConfig()internally, so the same defaults and the samewould_disable()/dict_config()apply to it — but this package does not know how to reach into Django's settings; pass theLOGGINGdict towould_disable()directly. - No
logging.config.listen()/ socket-based config support. Config arriving over the network is not something this looks at. - Does not touch
disable_existing_loggersitself.dict_configreports and can refuse, but never silently rewrites the config to change what gets disabled — that decision stays with the caller.
Tested locally on Python 3.14.7. pyproject.toml requires Python >= 3.11 and classifies 3.11–3.14; CI (.github/workflows/test.yml) runs the full check suite on Python 3.11, 3.12, 3.13, and 3.14.
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/python -m pytest -q
.venv/bin/python -m mypy src --strictTests exercise the real logging.config.dictConfig/fileConfig calls alongside keeploggers' own predictions, not just the internal model in isolation, and an autouse fixture snapshots and restores all the global logging state (Logger.manager.loggerDict, each logger's disabled/level/handlers/propagate, root's handlers, and the private logging._handlers registry) around every test.
MIT — see LICENSE.