• 8 min read

Python Log Levels: When and How to Use It

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.
Python log levels

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.

python
12345
import logging
logging.basicConfig()
logging.debug("This won't appear")
logging.info("This won't appear")
logging.warning("This will appear")
Default log level in Python

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:

python
12
logging.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.

python
1
logging.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:

python
123
logger = 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:

python
1246
logger = 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:

python
12345
import logging
logging.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:

python
1
logging.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.

python
1
NOTICE_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:

python
1
logging.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:

python
1
logger.log(NOTICE_LEVEL, "This is a notice message.")

For more idiomatic usage, a convenience method can be added to the logging.Logger class:

python
12456791112
NOTICE_LEVEL = 25
logging.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 = notice
logger = logging.getLogger(__name__)
logger.notice("a notice message")
Notice custom level in Python

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 or INFO, depending on test depth and performance needs.
  • Production: Use INFO or WARNING 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.