Python - logging

Introduction

Logging is essential for building dependable software. It records software events, creating an audit trail that details various system operations. This trail helps diagnose issues, understand runtime behavior, and analyze user interactions, offering insights into design decisions.

logging

Python’s built-in logging module is incredibly powerful and boasts the necessary logging features that one might require from a separate logging framework. Therefore, it remains the most popular way to log in the Python ecosystem despite the existence of a few third-party logging frameworks like Loguru.

Providing a logging module in the standard library ensures that all Python programs can benefit from having consistent logging features, making it easier to adopt and understand when working on different projects.

To get started with the logging module, you need to import it to your program first, as shown below:

import logging
logging.debug("A debug message")
logging.info("An info message")
logging.warning("A warning message")
logging.error("An error message")
logging.critical("A critical message")

When you execute the above program, you should observe the following output:

WARNING:root:A warning message
ERROR:root:An error message
CRITICAL:root:A critical message

Notice that the debug() and info() messages are missing from the output while the others are present. This is due to the default log level configured on the logging module, which we will get to shortly. For now, understand that this is the intended behavior of the example.

In each record above, you will observe that the log level (WARNING, ERROR, etc.) is printed in all caps followed by root, which is the name of the default logger. Finally, you have the log message that was passed as an argument to the function.

This output format is summarized below:

<LEVEL>:<name>:<message>
Task
  • Change log level to INFO using logging.basicConfig() so that all log messages with a severity level of INFO and above are displayed.

  • Change log level to ERROR usin g logging.basicConfig() so that only log messages with a severity level of ERROR and above are displayed.

Log levels

Log levels define the severity of the event that is being logged. They convey implicit meaning about the program state when the message was recorded, which is crucial when sieving through large logs for specific events.

For example, a message logged at the INFO level indicates a normal and expected event, while one that is logged at the ERROR level signifies that some unexpected error has occurred.

The available log levels in the logging module are listed below in increasing order of severity:

Here’s the table without the numeric values:

LevelDescription
DEBUGUsed to log messages that are useful for debugging.
INFOUsed to log events within the parameters of expected program behavior.
WARNINGUsed to log unexpected events which may impede future program function but are not severe enough to be an error.
ERRORUsed to log unexpected failures in the program. Often, an exception needs to be raised to avoid further failures, but the program may still be able to run.
CRITICALUsed to log severe errors that can cause the application to stop running altogether.

It’s important to always use the most appropriate log level so that you can quickly find the information you need.

For instance, logging a message at the WARNING level will help you find potential problems that need to be investigated, while logging at the ERROR or CRITICAL level helps you discover problems that need to be rectified immediately.

By default, the logging module will only produce records for events that have been logged at a severity level of WARNING and above.

This is why the debug() and info() messages were omitted in the previous example since they are less severe than WARNING.

You can change this behavior using the logging.basicConfig() method as demonstrated below:

import logging
logging.basicConfig(level=logging.INFO)
logging.debug("A debug message")
logging.info("An info message")
logging.warning("Something bad is going to happen")
logging.error("An error message")
logging.critical("A critical message")
Nota

Ensure to place the call to logging.basicConfig() before any methods such as info(), warning(), and others are used. It should also be called once as it is a one-off configuration facility. If called multiple times, only the first one will have an effect.

When you execute the program now, you will observe the output below:

INFO:root:An info message
WARNING:root:A warning message
ERROR:root:An error message
CRITICAL:root:A critical message

Since we’ve set the minimum level to INFO, we are now able to view the log message produced by logging.info() in addition levels with greater severity while the DEBUG message remains suppressed.

Setting a default log level in the manner shown above is useful for controlling the number of logs that are generated by your application.

For example, during development you could set it to DEBUG to see all the log messages produced by the application, while production environments could use INFO or WARNING.

Using log levels meaningfully

Imagine you are writing a small script that:

  • Reads a number from the user.
  • Divides 100 by that number.
  • Prints the result.

Add logging calls with appropriate levels:

  • When the program starts.
  • When you read the user input.
  • When the calculation succeeds.
  • When the user enters 0 and a division by zero would occur.

Questions:

  • Which messages should be INFO?
  • Which should be WARNING or ERROR? Justify your choice briefly.

Log format

Beyond the log message itself, it is essential to include as much diagnostic information about the context surrounding each event being logged. Standardizing the structure of your log entries makes it much easier to parse them and extract only the necessary details.

Generally, a good log entry should include at least the following properties:

PropertyDescription
TimestampIndicates the time the logged event occurred
Log levelIndicates the severity of the event
MessageDescribes the details of the event

These properties allow for basic filtering by log management tools so you must ensure they are present in all your Python log entries. At the moment, the timestamp is missing from our examples so far which prevents us from knowing when each log entry was created.

Let’s fix this by changing the log format through the logging.basicConfig() method as shown below:

import logging
logging.basicConfig(format="%(asctime)s | %(levelname)s | %(message)s")
logging.warning("Something bad is going to happen")

The format argument above configures how log messages are displayed.

It uses the following LogRecord attributes for formatting the logs:

AttributeDescription
%(asctime)sHuman-readable time indicating when the log was created.
%(levelname)sThe log level name (INFO, DEBUG, WARNING, etc.).
%(message)sThe log message.

With the above configuration in place, all log entries produced by the logging module will now be formatted in following manner:

2023-02-05 11:41:31,862 | WARNING | Something bad is going to happen

Notice that the root name no longer appears in the log entry because the log format has been redefined to exclude it. The fields are also now separated by spaces and the | character to make them easier to read.

After the log level, you have the log creation time which can be customized further using the datefmt argument to basicConfig():

import logging
logging.basicConfig(
format="%(levelname)s | %(asctime)s | %(message)s",
datefmt="%H:%M:%S,%f",
)
logging.warning("Something bad is going to happen")
WARNING | 2025-11-27T08:52:58Z | Something bad is going to happen

The datefmt argument accepts the same directives as the time.strftime() method.

The ISO-8601 format shown above it is widely recognized and supported, which makes it easy to process and sort timestamps consistently.

Please refer to the LogRecord documentation to examine all the available attributes that you can use to format your log entries.

Here’s another example that uses a few more of these attributes to customize the log format:

import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
logging.debug("Database query executed in 0.234ms: SELECT * FROM users WHERE id=42")
logging.info("User authentication successful: user_id=12345, username='john_doe'")
logging.warning("System disk is full")
logging.error("Failed to connect to database: Connection timeout")
2025-11-27 09:10:31 | INFO | main.py:10 | User authentication successful: user_id=12345, username='john_doe'
2025-11-27 09:10:31 | WARNING | main.py:11 | System disk is full
2025-11-27 09:10:31 | ERROR | main.py:12 | Failed to connect to database: Connection timeout

Custom loggers

So far, we’ve used the default logger (named root) to write logs in the examples.

The default logger is accessible and configurable through module-level methods on the logging module (such as logging.info(), logging.basicConfig()).

In general, it’s best to avoid using the root logger and instead create a separate logger for each module in your application. This allows you to easily control the logging level and configuration for each module, and provide better context for log messages.

Creating a new custom logger can be achieved by calling the getLogger() method as shown below:

import logging
logger = logging.getLogger("toto")
logger.info("An info")
logger.warning("A warning")

The getLogger() method returns a Logger object whose name is set to the specified name argument (toto in this case). A Logger provides all the functions for you to log messages in your application. Unlike the root logger, its name is not included in the log output by default. In fact, we don’t even get the log level; only the log message is present. However, it defaults to logging to the standard error just like the root logger.

To change the output and behavior of a custom logger, you have to use the Formatter and Handler classes provided by the logging module.

The Formatter does what you’d expect; it helps with formatting the output of the logs, while the Handler specifies the log destination which could be the console, a file, an HTTP endpoint, and more.

We also have Filter objects which provide sophisticated filtering capabilities for your Loggers and Handlers.

Pending

A Comprehensive Guide to Logging in Python

Tasks

First logging calls

Create a file basic_logging.py and:

  • Import the logging module.
  • Log one message at each level: DEBUG, INFO, WARNING, ERROR, CRITICAL.
  • Run the script.

Questions:

  • Which messages appear on the screen?
  • Why do some levels not appear?
Playing with log levels

Using the previous example (or a new file):

  1. Configure logging so that all messages from INFO and above are shown.
  2. Run it and verify the output.
  3. Reconfigure so that only messages from ERROR and above are shown.
  4. Run it again.

Questions:

  • For level=logging.INFO, which levels are visible?
  • For level=logging.ERROR, which levels disappear? Why?
Adding timestamps and custom format

Create a script formatted_logging.py that:

  • Calls logging.basicConfig(...) with:
    • level=logging.INFO
    • format="%(asctime)s | %(levelname)s | %(message)s"
  • Logs at least three messages:
    • An INFO like "Application started"
    • A WARNING like "Low disk space"
    • An ERROR like "Failed to open file"

Questions:

  • What information do you see before the actual message?
  • Which part of the format string corresponds to the timestamp, the level, and the message?
Replacing print with logging

You have this program:

number = int(input("Enter a number: "))
result = 100 / number
print("Result is:", result)

Rewrite it to:

  • Use logging instead of print for diagnostic messages.
  • Log:
    • When the program starts.
    • The number entered by the user.
    • The successful result.
    • If the user enters 0, catch the ZeroDivisionError and:
    • Log an ERROR with a clear message.
    • Do not let the program crash.

Questions:

  • Which messages should be INFO?
  • Which should be ERROR? Why?
Writing logs to a file

Create a script file_logging.py that:

  • Writes logs to a file named app.log.
  • Uses format: %(asctime)s - %(levelname)s - %(message)s
  • Uses level DEBUG.
  • Logs:
    • A DEBUG message when starting.
    • An INFO message for a normal event.
    • A WARNING for something suspicious.
    • An ERROR for a simulated failure.

Then run it and open app.log.

Questions:

  • Is the file appended to or overwritten each time?
  • Do you see all four levels? Why?
Custom loggers for modules

Create two files: main.py and utils.py.

In main.py:

  • Create a logger named "main".
  • Configure it with a console handler that uses:
    • Level: INFO
    • Format: '%(name)s - %(levelname)s - %(message)s'
  • Log an INFO message "Starting main script".
  • Import utils and call a function do_work() from it.

In utils.py:

  • Create a logger named "utils".
  • In do_work():
    • Log a DEBUG message "utils: starting helper function".
    • Log an INFO message "utils: helper completed".

Questions:

  • What logger names appear in the output?
  • Do you see the DEBUG message? If not, how could you change the level so it appears?
Different handlers for console and file

Create a script handlers_logging.py that:

  • Gets a logger named "shop" and sets its level to DEBUG.
  • Adds:
    • A console handler with:
      • Level: INFO
      • Format: '%(levelname)s - %(message)s'
    • A file handler with:
      • Level: DEBUG
      • Format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
      • File: shop.log
  • Logs:
    • DEBUG: "Cart total recalculated"
    • INFO: "User checked out"
    • WARNING: "Payment took too long"

Then run the script and compare the console output with shop.log.

Questions:

  • Which messages appear on the console?
  • Which messages appear in the file?
  • Why is there a difference?
Logging exceptions with tracebacks

Create errors_logging.py that:

  • Configures logging with:
    • level=logging.INFO
    • A format that includes %(levelname)s and %(message)s.
  • Asks the user for a filename.
  • Tries to open the file and read its contents.
  • On success, logs an INFO that the file was opened.
  • If opening fails, catches the exception and logs an ERROR with full traceback.

Questions:

  • What extra information do you see when using logging.exception compared to logging.error?
  • Why is this useful in real applications?