Skip to main content

Command Palette

Search for a command to run...

Python Logging Dict Configuration

Learn how to use logging.cofig.dictConfig in your application

Updated
•8 min read•View as Markdown
R

As a seasoned senior web developer with a wealth of experience in Python, Javascript, web development, MySQL, MongoDB, and React, I am passionate about crafting exceptional digital experiences that delight users and drive business success.

When you are working on a big and complex project, using basicConfig will not be sufficient. You need to use something more robust, configurable, and flexible. Adding all the handlers, formatters, and logger manually generates a lot of code that can be hard to maintain or update. The dictConfig function of the Python logging module provides an easy way to customize logging in your complex project.

Unlike basicConfig, dictConfig provides a lot more options to the developers to configure logging that suits big applications. dictConfig is a function offered by the standard Python logging library. As the name suggests, the developer creates a dictionary where all the configurations are written. This function allows managing loggers of installed packages as well.

The dictConfig is declarative. A developer needs to specify what is required, and the implementation is handled by the logging module. But basicConfig is imperative. A developer needs to provide step-by-step instructions on how to implement everything.

Please read the Basic Configuration Blog First: https://ritwikmath.hashnode.dev/python-logging-basic-configuration.

Simple Configuration:

A Developer can configure the root logger through dictConfig

import logging
import logging.config

# Complete logging configuration dictionary
LOG_CONFIG = {
    'version': 1,  # Required - schema version
    'disable_existing_loggers': False,  # Keep existing loggers active
    # Define how log messages should be formatted
    'formatters': {
        'simple': {
            'format': '%(asctime)s [%(levelname)s] %(message)s',
            'datefmt': '%Y-%m-%d %H:%M:%S'
        },
    },
    # Define where log messages should go
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',  # Built-in handler
            'formatter': 'simple',
            'stream': 'ext://sys.stdout'  # External object reference
        }
    },
    # Root logger configuration (parent of all loggers)
    'root': {
        'level': 'DEBUG',
        'handlers': ['console']
    }
}

# Apply the configuration
logging.config.dictConfig(LOG_CONFIG)

logging.info('I am root logger')

The version has to always be set to 1 . It is better to set disable_existing_loggers to False if the developer is sure about the outcome.

Formatters

The formatters are the key where a developer will configure the format of the messages. In basicConfig, you only need to configure the format once. However, in dictConfig, the developer must do this for every formatter.

'formatters': {
    'simple': {
        'format': '%(asctime)s [%(levelname)s] %(message)s',
        'datefmt': '%Y-%m-%d %H:%M:%S'
    },
    'standard': {
        'format': '{asctime} ({funcName}) [{levelname}] {message} {name}',
        'style': '{',
        'datefmt': '%Y-%m-%d %H:%M:%S'
    }
},

Handlers

In basicConfig, Python created a streamhandler or fileHandler behind the scenes and attached it to the root logger. In the case of dictConfig, the developer needs to configure everything from scratch.

'handlers': {
    'console': {
        'class': 'logging.StreamHandler',  # Built-in handler
        'formatter': 'standard',
        'stream': 'ext://sys.stdout'  # External object reference
    }
},

The class key refers to which logging handler to use. For printing on the console, logging.StreamHandler is an ideal choice and is offered by the logging module. The stream is to provide a reference to the object where the log output will be written.

In case a developer wants to implement a FileHandler, the configuration would be as follows.

 'handlers': {
    'file': {
        'class': 'logging.FileHandler',  # Built-in handler
        'formatter': 'standard',
        'filename': 'access.log'
    }
},

The developer can configure the mode (filemode), encoding, and errors as well, based on the requirement.

Instead of printing the log on the console directly, the developer can store it in an io and later do something with it.

***
from io import StringIO

sio = StringIO()

***

'handlers': {
    'console': {
        'class': 'logging.StreamHandler',  # Built-in handler
        'formatter': 'standard',
        'stream': sio  # External object reference
    }
},

***

logging.info('I am root logger')

sio.seek(0)

print(sio.read().strip())

Custom Logger

The dictConfig function allows generating custom loggers. In the previous blog, I showed how to create a custom logger using the getLogger method.

import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s %(name)s'
)

app_logger = logging.getLogger('app')

app_logger.info('Working')

# Log record
# 2025-09-26 12:35:15,040 [INFO] Working app

In dictConfig you do it within the configuration.

'loggers': {
    'app': {
        'level': 'INFO',
        'handlers': ['console'],
        'propagate': False,
    }
},

The final code is

import logging
import logging.config
from io import StringIO

# Complete logging configuration dictionary
LOG_CONFIG = {
    'version': 1,  # Required - schema version
    'disable_existing_loggers': False,  # Keep existing loggers active
    # Define how log messages should be formatted
    'formatters': {
        'standard': {
            'format': '{asctime} ({funcName}) [{levelname}] {message} {name}',
            'style': '{',
            'datefmt': '%Y-%m-%d %H:%M:%S'
        }
    },
    # Define where log messages should go
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',  # Built-in handler
            'formatter': 'standard',
            'stream': 'ext://sys.stdout'  # External object reference
        }
    },
    'loggers': {
        'app': {
            'level': 'INFO',
            'handlers': ['console'],
            'propagate': False,
        }
    },
    # Root logger configuration (parent of all loggers)
    'root': {
        'level': 'DEBUG',
        'handlers': ['console']
    }
}

# Apply the configuration
logging.config.dictConfig(LOG_CONFIG)

app_logger = logging.getLogger('app')

app_logger.info('Working')

# Log record
# 2025-09-26 12:35:15,040 [INFO] Working app

A developer can also use hierarchical logging. If propagate is not set as False, the child logger inherits configuration from the parent logger.

'loggers': {
    'app': {
        'level': 'DEBUG',
        'handlers': ['file'],
        'propagate': False,
    },
    'app.api': {
        'level': 'INFO',
        'handlers': ['console'],
        'propagate': False # Default is True
    },
    'app.db': {
        'handlers': ['console']
    }
},

Final code is

import logging
import logging.config
from io import StringIO

# Complete logging configuration dictionary
LOG_CONFIG = {
    'version': 1,  # Required - schema version
    'disable_existing_loggers': False,  # Keep existing loggers active
    # Define how log messages should be formatted
    'formatters': {
        'standard': {
            'format': '{asctime} ({funcName}) [{levelname}] {message} {name}',
            'style': '{',
            'datefmt': '%Y-%m-%d %H:%M:%S'
        }
    },
    # Define where log messages should go
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',  # Built-in handler
            'formatter': 'standard',
            'stream': 'ext://sys.stdout'  # External object reference
        },
        'file': {
            'class': 'logging.FileHandler',
            'formatter': 'standard',
            'filename': 'access.log',
            'mode': 'a' # Default is append (a)
        }
    },
    'loggers': {
        'app': {
            'level': 'DEBUG',
            'handlers': ['file'],
            'propagate': False,
        },
        'app.api': {
            'level': 'INFO',
            'handlers': ['console'],
            'propagate': False # Default is True
        },
        'app.db': {
            'handlers': ['console']
        }
    },
    # Root logger configuration (parent of all loggers)
    'root': {
        'level': 'DEBUG',
        'handlers': ['console']
    }
}

# Apply the configuration
logging.config.dictConfig(LOG_CONFIG)

api_logger = logging.getLogger('app.api')
db_logger = logging.getLogger('app.db')

# Test logging with extra data
api_logger.info('Hi')
db_logger.debug('Fetched user details from database')

# Log record
# 2025-09-26 12:51:05 (<module>) [INFO] Hi app.api
# 2025-09-26 12:51:05 (<module>) [DEBUG] Fetched user details from database app.db

access.log

2025-09-26 12:51:05 (<module>) [DEBUG] Fetched user details from database app.db

Custom Classes

Custom Formatter

In Python's logging module, a custom formatter is a class that inherits from logging.Formatter and allows a developer to define a specific layout and content for log messages beyond the default logging.Formatter provides.

# Custom JSON formatter for structured logging
class JSONFormatter(logging.Formatter):
    def format(self, record):
        log_entry = {
            'timestamp': datetime.fromtimestamp(record.created).isoformat(),
            'level': record.levelname,
            'logger': record.name,
            'message': record.getMessage(),
            'module': record.module,
            'function': record.funcName,
            'line': record.lineno,
            'thread': record.thread,
            'process': record.process
        }

        # Add extra fields if they exist (passed via extra parameter)
        if hasattr(record, 'query_result'):
            log_entry['data_set'] = record.query_result

        return json.dumps(log_entry)

JSONFormatter creates a string that stores the log message in json format. A developer can add this formatter to the config file.

'formatters': {
    'standard': {
        'format': '{asctime} ({funcName}) [{levelname}] {message} {name}',
        'style': '{',
        'datefmt': '%Y-%m-%d %H:%M:%S'
    },
    'jsonformatter': {
        '()': JSONFormatter
    }
},

Custom Handler

It works the same way as a custom formatter. Inherit from logging.Handler class and update the configuration.

class CustomHandler(logging.Handler):
    def emit(self, record):
        if record.levelno >= logging.ERROR:
            filename = 'error.log'
        else:
            filename = 'access.log'

        with open(filename, 'a') as file:
            message = self.format(record)
            file.write(message + '\n')

Final code after adding custom Formatter and Handler


import json
import logging
import logging.config
from io import StringIO
from datetime import datetime

# Custom JSON formatter for structured logging
class JSONFormatter(logging.Formatter):
    def format(self, record):
        log_entry = {
            'timestamp':datetime.fromtimestamp(record.created).isoformat(),
            'level': record.levelname,
            'logger': record.name,
            'message': record.getMessage(),
            'module': record.module,
            'function': record.funcName,
            'line': record.lineno,
            'thread': record.thread,
            'process': record.process
        }

        # Add extra fields if they exist (passed via extra parameter)
        if hasattr(record, 'query_result'):
            log_entry['data_set'] = record.query_result

        return json.dumps(log_entry)

class CustomHandler(logging.Handler):
    def emit(self, record):
        if record.levelno >= logging.ERROR:
            filename = 'error.log'
        else:
            filename = 'access.log'

        with open(filename, 'a') as file:
            message = self.format(record)
            file.write(message + '\n')

# Complete logging configuration dictionary
LOG_CONFIG = {
    'version': 1,  # Required - schema version
    'disable_existing_loggers': False,  # Keep existing loggers active
    # Define how log messages should be formatted
    'formatters': {
        'standard': {
            'format': '{asctime} ({funcName}) [{levelname}] {message} {name}',
            'style': '{',
            'datefmt': '%Y-%m-%d %H:%M:%S'
        },
        'jsonformatter': {
            '()': JSONFormatter
        }
    },
    # Define where log messages should go
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',  # Built-in handler
            'formatter': 'jsonformatter',
            'stream': 'ext://sys.stdout'  # External object reference
        },
        'file': {
            '()': CustomHandler,
            'formatter': 'standard',
        }
    },
    'loggers': {
        'app': {
            'level': 'DEBUG',
            'handlers': ['file'],
            'propagate': False,
        },
        'app.api': {
            'level': 'INFO',
            'handlers': ['console'],
            'propagate': False # Default is True
        },
        'app.db': {
            'handlers': ['console']
        }
    },
    # Root logger configuration (parent of all loggers)
    'root': {
        'level': 'DEBUG',
        'handlers': ['console']
    }
}

# Apply the configuration
logging.config.dictConfig(LOG_CONFIG)

api_logger = logging.getLogger('app.api')
db_logger = logging.getLogger('app.db')

# Test logging with extra data
api_logger.info('Hi')
db_logger.debug('Fetched user details from database')
db_logger.error('Connection timeout')

# Log Record
# {"timestamp": "2025-09-26T13:14:20.753802", "level": "INFO", "logger": "app.api", "message": "Hi", "module": "main", "function": "<module>", "line": 96, "thread": 14688, "process": 27300}
# {"timestamp": "2025-09-26T13:14:20.754786", "level": "DEBUG", "logger": "app.db", "message": "Fetched user details from database", "module": "main", "function": "<module>", "line": 97, "thread": 14688, "process": 27300}
# {"timestamp": "2025-09-26T13:14:20.754786", "level": "ERROR", "logger": "app.db", "message": "Connection timeout", "module": "main", "function": "<module>", "line": 98, "thread": 14688, "process": 27300}

access.log

2025-09-26 13:14:20 (<module>) [DEBUG] Fetched user details from database app.db

error.log

2025-09-26 13:14:20 (<module>) [ERROR] Connection timeout app.db
L

Great guide on Python's dictConfig! It really simplifies logging setup, especially for larger projects. For even smoother local development, I’ve started using ServBay (servbay.com) — it lets me spin up isolated environments quickly, so I can test changes without worrying about my main setup.

1
R

Servbay seems like a good tool

The Definitive Guide to Python Logging Series

Part 4 of 4

This blog series is a comprehensive guide to Python's logging module, explaining core concepts and best practices.

Start from the beginning

Logging the Basics

What is Logging? Why you need to learn it as a developer?