In Python application development, the logging module offers a powerful and flexible way to generate and manage log messages. At the core of this system are log levels, which classify messages by severity.
Using log levels effectively serves several key purposes:
- It filters out unnecessary details in production while enabling deep diagnostics during development.
- It helps you focus on relevant issues, reducing noise and speeding up debugging.
- It enables quick identification of notable or critical events, which leads to faster resolution.
What are log levels in Python?
Log levels in Python are predefined constants that indicate the severity of a log message. They allow you to categorize logs and control output based on importance.
The standard Python log levels include:
DEBUG
(10): Captures detailed diagnostic information, typically only of interest during development and troubleshooting.INFO
(20): Used to convey messages that confirm things are working as expected or to provide general information about the application’s operational flow.WARNING
(30): Used indication of potential issues that may require attention.ERROR
(40): Records problems that impact some functionality of the application.CRITICAL
(50): Reserved for the most severe error conditions, indicating that the program itself may be unable to continue running or that a catastrophic failure has occurred.
These levels are defined as integer values, with DEBUG
being the lowest (10) and CRITICAL
the highest (50).
There’s also a special NOTSET
value (0) which indicates that the logger should delegate the decision of which messages to process to its parent in the logger hierarchy. If this delegation chain reaches the root logger and the root is also NOTSET
, all messages will be processed.
The default log level in Python
By default, if no explicit configuration is provided for the logging module, the root logger is configured with a level of WARNING.
This means only WARNING, ERROR, and CRITICAL messages will be shown while lower-level messages like DEBUG and INFO are ignored.
12345import logginglogging.basicConfig()logging.debug("This won't appear")logging.info("This won't appear")logging.warning("This will appear")
This default setting prioritizes essential messages, keeping log output to a minimum unless more detail is explicitly requested.
If you wish to see DEBUG
or INFO
messages, you must explicitly configure the logging system to a lower threshold:
12logging.basicConfig(level=logging.DEBUG)logging.debug("Now this will appear")
Customizing log levels in Python
Python’s logging module enables fine-grained control over log levels for the root logger, custom loggers, and handlers. Let’
Setting the root logger level
You’ve already see the usage of logging.basicConfig()
to to set the root logger’s level.
1logging.basicConfig(level=logging.DEBUG)
However, it’s important to note that basicConfig()
can only be called effectively once to configure the root logger. If the root logger already has handlers, basicConfig()
will do nothing unless the force=True
keyword argument is set
Setting levels for custom loggers
When a custom logger is created, its level is initially NOTSET
which means the effective level is inherited from its parent logger or ultimately the root logger.
You can also set an explicit level using the setLevel()
method like this:
123logger = logging.getLogger(__name__)logger.setLevel(logging.DEBUG)logger.debug("a debug message")
The logger.getEffectiveLevel()
method can be used to determine the actual operational level of a logger after considering this hierarchical inheritance.
Additionally, logger.isEnabledFor(level)
can check if a message of a given level would be processed by that logger, considering both its effective level and any module-level disable()
calls.
Setting levels for handlers
Handlers are responsible for dispatching log records to the appropriate destination (e.g., console or file).
Each handler can also have its own log level, set using its handler.setLevel(level)
method:
1246logger = logging.getLogger(__name__)stdout = logging.StreamHandler(stream=sys.stdout)stdout.setLevel(logging.DEBUG)logger.addHandler(stdout)
A message must first pass the logger’s level check to be dispatched to its handlers, and then it must pass the individual handler’s level check to be output by that specific handler.
By default, when a handler is created, its level is NOTSET
, meaning it will process all messages it receives from its logger.
Disabling logging globally
The logging module provides a disable()
method which acts as a global override for all loggers. It defaults to CRITICAL
which means that no logs will appear regardless of individual logger settings:
12345import logginglogging.disable()logging.debug("This won't appear")logging.warning("This won't appear")logging.critical("This won't appear either")
For example, you might use this to disable all non-error logs regardless of logger configuration:
1logging.disable(logging.WARNING) # warning logs and below will never show up
Calling logging.disable(logging.NOTSET)
removes this global override, allowing your logging output to once again depend on the effective levels of individual loggers.
Defining and using custom log levels
While Python’s logging module provides a comprehensive set of standard log levels, it is technically possible to define custom log levels.
However, this practice is generally discouraged by the official documentation since the standard levels already cover most common use cases.
If custom levels are deemed necessary, the process involves two main steps:
Choose a numeric value
A unique integer must be chosen for the custom level which ideally be placed numerically between existing standard levels.
1NOTICE_LEVEL = 25
Register the level
The chosen numeric value and a corresponding name for the custom level must be registered with the logging system using:
1logging.addLevelName(NOTICE_LEVEL, "NOTICE")
This function associates the number with the name, allowing formatters to display the custom level name in log output.
Once defined and registered, you can use the custom level with the logger.log()
method:
1logger.log(NOTICE_LEVEL, "This is a notice message.")
For more idiomatic usage, a convenience method can be added to the logging.Logger
class:
12456791112NOTICE_LEVEL = 25logging.addLevelName(NOTICE_LEVEL, "NOTICE")def notice(self, message, *args, **kws):if self.isEnabledFor(NOTICE_LEVEL):# Use self._log() to ensure correct stack frame information for lineno, funcName etc.self._log(NOTICE_LEVEL, message, args, **kws)logging.Logger.notice = noticelogger = logging.getLogger(__name__)logger.notice("a notice message")
Best practices for setting Python log levels
Adopting the following best practices for setting log levels can significantly enhance the utility of your Python application logs:
1. Match log levels to the environment
- Development: Use
DEBUG
to capture detailed execution flow and diagnostics. - Testing/Staging: Use
DEBUG
orINFO
, depending on test depth and performance needs. - Production: Use
INFO
orWARNING
to reduce noise and avoid performance impact.
2. Log with purpose
Only log what’s necessary for understanding and diagnosing issues. Avoid overly verbose or sparse logs. Choose levels that reflect the true severity of events.
3. Use module-level loggers
Always use logging.getLogger(<name>)
at the module level. This allows different parts of an application to control logging independently without interfering with others.
4. Respect library logging etiquette
Libraries should:
- Use
getLogger(__name__)
. - Add a
NullHandler
by default. - Avoid configuring the root logger.
This lets applications decide how library logs are handled, following the principle of separation of concerns.
5. Log exceptions appropriately
- Use
logger.error()
for handled exceptions. - Use
logger.exception()
to include the stack trace. - Use
logger.critical()
for unrecoverable errors.
6. Avoid custom log levels
Stick to standard levels (DEBUG
, INFO
, WARNING
, ERROR
, CRITICAL
) for consistency and maintainability.
7. Design for flexibility
Logging configurations should be environment-driven. Use environment variables, config files, or CLI flags to ensure log levels can be adjusted dynamically.
Final thoughts
Properly using Python log levels helps manage the visibility of log messages and ensures meaningful insights during debugging or monitoring.
In this sense, log levels are a foundational element in building observable systems, where the internal state and behavior of an application can be effectively understood, monitored, and managed throughout its lifecycle.