Python’s logging system becomes easier to maintain when configuration is separated from application logic. logging.config.dictConfig() lets you define formatters, handlers, logger levels, propagation rules, and output destinations in one structured dictionary instead of scattering setup code across a program.
This lesson continues the logging sequence after OSPython.033: Logging Filters, OSPython.034: Contextual Logging, and OSPython.035: Logging Exceptions. The goal now is to configure the whole logging graph in one place.
Learning Objectives
- Explain what
dictConfig()configures. - Build formatter, handler, and logger sections correctly.
- Understand the required
versionkey. - Use
disable_existing_loggersdeliberately. - Route INFO and ERROR records to different handlers.
- Load logging configuration from JSON safely.
- Verify the active configuration with real log records.
Why Move Beyond basicConfig()
logging.basicConfig() is useful for small scripts, but larger applications often need multiple handlers, different levels, file output, structured formatting, and separate logger namespaces. Writing all of that imperatively works, but it can become repetitive and difficult to audit.

dictConfig() treats logging setup as data. That makes the configuration easier to inspect, test, reuse, serialize, and replace for different environments such as local development, staging, and production.
The Smallest Useful dictConfig
import logging
import logging.config
CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(asctime)s %(levelname)s %(name)s %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "standard",
"level": "INFO",
}
},
"root": {
"handlers": ["console"],
"level": "INFO",
},
}
logging.config.dictConfig(CONFIG)
logger = logging.getLogger(__name__)
logger.info("Logging is configured")
The version key is required by the configuration schema. Today the documented value is 1. The formatter is named standard, the handler is named console, and the root logger sends records to that handler at INFO and above.
Formatters Define Record Layout
The formatters section defines how a LogRecord becomes readable output. A formatter can include timestamps, levels, logger names, process IDs, filenames, line numbers, or custom contextual fields. Formatter names are local configuration identifiers; handlers refer to them by those names.
"formatters": {
"detailed": {
"format": (
"%(asctime)s %(levelname)s %(name)s "
"%(filename)s:%(lineno)d %(message)s"
)
}
}
Handlers Decide Where Records Go
Handlers determine the destination. A StreamHandler can write to standard output or standard error. A FileHandler writes to a file. Rotating handlers from earlier lessons can also be referenced by class name inside a dictionary configuration.
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "detailed",
"level": "INFO",
"stream": "ext://sys.stdout",
},
"errors": {
"class": "logging.FileHandler",
"formatter": "detailed",
"level": "ERROR",
"filename": "errors.log",
"encoding": "utf-8",
},
}
The special ext://sys.stdout syntax tells the logging configuration system to resolve an existing Python object instead of treating the value as an ordinary string.
Loggers Connect Names to Handlers
The loggers section controls named logger namespaces. A service can give app.api and app.worker different handlers or levels while still sharing common formatting. Logger hierarchy still applies, so propagation must be deliberate.
"loggers": {
"app.api": {
"handlers": ["console", "errors"],
"level": "DEBUG",
"propagate": False,
}
}
Setting propagate to False prevents a record handled by app.api from also traveling upward to ancestor handlers and appearing twice. Duplicate output is one of the most common signs that logger hierarchy was configured without checking propagation.
Use disable_existing_loggers Carefully
disable_existing_loggers deserves deliberate attention. If set to True, loggers that already exist and are not explicitly covered by the new configuration may be disabled. That can unexpectedly silence messages from libraries. Setting it to False is often safer when an application depends on third-party packages whose loggers should continue to operate.
Load Configuration From JSON
A dictionary can be created directly in Python, but it can also come from JSON because JSON objects map naturally to Python dictionaries. This allows operations teams to change levels or destinations without editing application code.
import json
import logging.config
from pathlib import Path
config_path = Path("logging.json")
with config_path.open("r", encoding="utf-8") as handle:
config = json.load(handle)
logging.config.dictConfig(config)
Do not treat external configuration as automatically trustworthy. Validate file location, deployment ownership, expected keys, handler destinations, and permissions. A logging configuration can control file paths and importable classes, so configuration changes deserve the same operational discipline as other application settings.
Related social discussion: Emily Riederer recommends the mCoding logging tutorial for its modern mental model, best practices, and customization.
Production Example: Console Plus Error File
CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(asctime)s %(levelname)s %(name)s %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "standard",
"level": "INFO",
"stream": "ext://sys.stdout",
},
"errors": {
"class": "logging.FileHandler",
"formatter": "standard",
"level": "ERROR",
"filename": "errors.log",
"encoding": "utf-8",
},
},
"root": {
"handlers": ["console", "errors"],
"level": "DEBUG",
},
}
With this setup, INFO and WARNING records appear on the console. ERROR and CRITICAL records appear on the console and are also written to errors.log. DEBUG records pass the root logger’s level test but are rejected by both handlers because their handler thresholds are higher. Logger level and handler level are separate gates.
Verification Checklist
- Emit one DEBUG record and confirm whether any handler accepts it.
- Emit one INFO record and verify the console output.
- Emit one ERROR record and verify both console and file output.
- Check that the formatter includes the expected timestamp, level, logger name, and message.
- Create a child logger and verify whether propagation causes duplicate output.
- Temporarily change a handler level and confirm that routing changes as expected.
- Restart the program and verify that the configuration is loaded consistently.
Common Failure Modes
- Unknown formatter name: a handler references a formatter key that does not exist.
- Unknown handler name: a logger references a handler key that does not exist.
- Wrong class path: the configured handler class cannot be imported.
- Duplicate records: a child logger handles a record and then propagates it to an ancestor with another handler.
- Silent libraries:
disable_existing_loggers=Truedisables loggers you expected to keep. - Missing file permissions: a file handler cannot open or create its target file.
Hands-On Lab
- Create a new Python file and import
loggingandlogging.config. - Define one formatter named
standard. - Create a console handler at INFO.
- Create a file handler at ERROR.
- Configure the root logger at DEBUG.
- Emit DEBUG, INFO, WARNING, ERROR, and CRITICAL records.
- Explain why each record does or does not appear in each destination.
- Move the dictionary into a JSON file and load it with
json.load(). - Create a named logger with
propagate=Falseand confirm that duplicate messages disappear.
Knowledge Check
- What module contains
dictConfig()? - What value is normally used for the required
versionkey? - What is the difference between a formatter and a handler?
- Why can
propagate=Trueproduce duplicate messages? - Why can
disable_existing_loggers=Truebe risky? - Can JSON be used as a source for a logging configuration dictionary?
- If the root logger is DEBUG but a handler is INFO, will a DEBUG record reach that handler?
Answer Guide
logging.config.1.- A formatter defines record layout; a handler decides where records are emitted and can impose its own level.
- The child logger’s handlers may emit the record and then ancestor handlers can emit the same record again.
- It may disable third-party or previously created loggers that are not explicitly named in the configuration.
- Yes. JSON objects can be loaded into Python dictionaries and passed to
dictConfig(). - No. The handler rejects it because DEBUG is below the handler’s INFO threshold.
Key Takeaway
dictConfig() turns logging setup into a structured configuration graph. Formatters define representation, handlers define destinations, loggers define namespaces and routing, and levels determine which records survive each stage.
References
Primary references: Python documentation — Logging Configuration and Python Logging Cookbook.
BitcoinVersus.Tech
Advertisement
BitcoinVersus.Tech Editor’s Note:
We volunteer daily to ensure the credibility of the information on this platform is Verifiably True. If you would like to support our independent technical education work, please donate Bitcoin here: 3C9o19EH5HSiwEPyCTmEKzxhNCbo2X6TTb
BitcoinVersus.tech is not a financial advisor. This lesson is for informational and educational purposes.

Leave a Reply