When should an application use logging.config.dictConfig instead of logging.basicConfig?
answer
- One place, decided at startup
- The first writer wins, silently
- A declarative schema, not a helper call
- basicConfig no-ops when root has handlers
- disable_existing_loggers defaults to True
basics
~20 sUse basicConfig for a script: one call, one stderr handler, one level. Use dictConfig for an application, because it declares formatters, handlers and per-logger levels as data applied once at the entry point, and it can be reapplied.
solid answer
~40 s`logging.basicConfig()` is a convenience: it attaches a `logging.StreamHandler` writing to stderr plus a formatter to the root logger and sets the root's level - **but only if the root has no handlers yet**, unless you pass `force=True` (3.8+). Anything that logs before your setup runs, including a module-level `logging.info()` call, triggers an implicit `basicConfig()`, so the first writer wins and the process keeps that accidental configuration. `logging.config.dictConfig()` takes the whole configuration as a dict - `version: 1`, formatters, filters, handlers, per-logger levels and the root - applied once at the process entry point and reapplicable later. Its trap is `disable_existing_loggers`, which defaults to `True` and silences every logger created before the call, typically third-party ones bound at import; set it to `False`.
code
python · 11 linesimport logging.config
logging.config.dictConfig({
"version": 1,
"disable_existing_loggers": False,
"formatters": {"plain": {"format": "%(asctime)s %(levelname)s %(name)s %(message)s"}},
"handlers": {"stderr": {"class": "logging.StreamHandler", "formatter": "plain", "level": "INFO"}},
"loggers": {"ingest": {"level": "DEBUG"}},
"root": {"handlers": ["stderr"], "level": "WARNING"},
})
logging.getLogger("ingest.worker").info("configured once at startup")go deeper
Know that basicConfig is the one-line setup for a script and that it is called once, at the start of the program. Recognise dictConfig as the dictionary-based configuration an application uses instead.
Explain what basicConfig actually does - a stream handler plus formatter on the root, and the root's level - and why a second call does nothing without force=True. Be able to read a dictConfig schema and say what each section configures.
Show the production judgement: configure once at the entry point, never at import; keep libraries configuration-free; set disable_existing_loggers to False; and know how to reapply configuration to change a level on a running service.
Own the convention across services: one configuration source shared by the team, levels driven by deployment data rather than code edits, an agreed policy for who may raise verbosity at runtime, and the volume and retention cost that policy implies.
### The two APIs `logging.basicConfig(**kwargs)` exists so a script can be usefully configured in one line: `logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s")`. It builds a `logging.StreamHandler` on stderr (or a file handler with `filename=`), gives it a `logging.Formatter`, attaches it to the root logger and sets the root logger's level. `logging.config.dictConfig(config)` takes a declarative schema: a mandatory `"version": 1`, plus `formatters`, `filters`, `handlers`, `loggers` and `root` sections whose entries reference each other by name. Handlers name their class with a dotted path (`"class": "logging.StreamHandler"`), or a `"()"` key for an arbitrary factory. The whole thing is data, so it can live in a config file, be assembled from environment variables, or differ per deployment. `logging.config.fileConfig()` is the older ini-file form of the same idea and is less capable; new code uses `dictConfig`. ### The failure that makes teams switch The hazard in `basicConfig` is one sentence in its contract: **it does nothing if the root logger already has handlers.** It is not an error, not a warning - a silent no-op. Picture a log-ingest pipeline maintained by an eleven-person team. One shared helper module runs `logging.basicConfig(level=logging.INFO)` at import time; another calls the module-level `logging.warning(...)` while loading, which itself calls `basicConfig()` internally because the root has no handlers yet. By the time the service's `main()` runs its own `basicConfig(level=logging.DEBUG, filename=...)`, the root already has a handler, so that call returns having changed nothing. The process runs on a stale configuration frozen by whichever import happened to log first - and because import order shifts when someone adds a dependency, the configuration changes for reasons that have nothing to do with logging. `force=True` (3.8+) removes and closes the existing root handlers before reconfiguring, which is the escape hatch, but the real fix is to configure once, explicitly, at the entry point. ### What dictConfig buys **Per-logger levels.** The chatty third-party client goes to WARNING while your own package sits at DEBUG, expressed as data rather than as a pile of `setLevel` calls scattered through the code. **Multiple destinations with their own levels and formats**, declared side by side and reviewable in one diff - which is the point when eleven people share one file. **Repeatability.** The same dict can be applied again to reconfigure a running process, for example from a signal handler that rereads the level from a config file, or in a test that needs a known logging state. **One obvious place to look.** The question "why is this line at this level going here" has a single answer instead of a grep across the codebase. ### The dictConfig trap `disable_existing_loggers` defaults to **`True`**. Any logger object that already exists and is not named in the new configuration is marked disabled - and third-party libraries create their loggers at import time, which is almost always before your entry point runs. The symptom is that a dependency's logging goes completely quiet after configuration, with nothing in the output to explain it. Set `"disable_existing_loggers": False` unless you specifically want the old behaviour. `fileConfig` has the same default and the same trap. There is also `"incremental": True`, which applies only level changes to existing objects instead of rebuilding everything. ### The rules that follow Configure exactly once, at the process entry point - after argument parsing, before anything serves traffic. Libraries never configure: they call `logging.getLogger(__name__)` and, if they want to be tidy about an unconfigured host application, attach `logging.NullHandler()` to their own top-level logger. Keep configuration out of import time entirely, so import order cannot decide it. For an operational level switch, reapply `dictConfig` on a signal or read the level from an environment variable at startup rather than editing code. (`logging.config.listen()` exists for remote reconfiguration but opens a socket that executes configuration data, so treat it as a security decision, not a convenience.) And keep the small case small: a fifty-line script wants `basicConfig`, not a schema. The switch is warranted when there is more than one destination, more than one level, or more than one person editing the setup.
- A dependency's log lines vanish the moment your dictConfig call runs. What is the first thing you check?`disable_existing_loggers`, which defaults to `True`. Every logger object created before the call and not named in the new configuration is marked disabled, and libraries build their loggers at import time, so they are almost always in that set. Setting it to `False` restores them. Nothing is printed when this happens, which is why the symptom looks like the dependency stopped logging rather than like a configuration choice.
- Why can calling logging.basicConfig() in main() change nothing at all?Because `basicConfig` returns immediately when the root logger already has handlers. Any earlier import that configured logging, or that called a module-level function such as `logging.warning()` - which invokes `basicConfig()` itself when the root has no handlers - has already installed one. Pass `force=True` (3.8+) to tear down and replace the existing root handlers, or, better, configure once at the entry point and never at import time.
- How do you change a running service's log level without a redeploy?Keep the configuration as data and reapply it: read the level from an environment variable or a config file at startup, and reapply `logging.config.dictConfig` - optionally with `"incremental": True`, which adjusts levels on existing objects rather than rebuilding handlers - from a signal handler or an authenticated admin endpoint. Avoid `logging.config.listen()`, which opens a socket that executes configuration data, unless you have secured and verified that channel.
saying these in an interview costs you the question
- basicConfig can be called again to change the level
- dictConfig is just a verbose basicConfig
- Libraries should configure logging at import
- disable_existing_loggers defaults to False
- Configuring inside module import is fine
- Changing a level always needs a redeploy