Your first log message¶
In this tutorial, you will learn to use the logging module in Python to write log messages.
By the end, you will understand why logging is more useful than print() for diagnostic output,
and you will be able to configure basic logging and create named loggers.
Time commitment: 15--20 minutes
Prerequisites:
- Python 3.12 or later installed
- Basic Python knowledge (variables, functions, imports)
Learning objectives¶
By the end of this tutorial, you will be able to:
- Explain why the
loggingmodule is more useful thanprint()for diagnostic output - Use
logging.basicConfig()to set up quick logging configuration - Write log messages at different severity levels
- Create a named logger with
logging.getLogger()
Why use logging?¶
When something goes wrong in a program, how do you find out what happened? Many developers
reach for print() first. It is quick, familiar, and gets the job done for small scripts.
However, print() has limitations:
- There is no way to filter messages by severity
- Output always goes to the same place (standard output)
- There is no easy way to disable diagnostic output in production
- Messages lack context such as timestamps, source locations, and severity
The logging module in Python solves all of these problems. It is part of the standard library,
so there is nothing extra to install.
A quick comparison¶
Let us compare print() and logging side by side.
# Using print() for diagnostic output
print("Starting data processing")
print("Processed 10 items")
print("WARNING: 2 items were skipped")
print("ERROR: Could not save results")
import logging
# Using logging for diagnostic output
logging.info("Starting data processing")
logging.info("Processed 10 items")
logging.warning("2 items were skipped")
logging.error("Could not save results")
Notice that only WARNING and ERROR appeared in the logging output. By default, the
logging module only shows messages at WARNING level or above. The info() messages
were silently filtered out.
This is one of the key advantages of logging: you can control which messages appear without changing your code.
The logging module¶
The logging module is built into Python. You do not need to install anything – just
import it and start using it.
The simplest way to use logging is through the module-level convenience functions:
logging.debug()-- Detailed diagnostic informationlogging.info()-- General information about program operationlogging.warning()-- Something unexpected happened (this is the default level)logging.error()-- An error occurred but the program can continuelogging.critical()-- A serious error; the program may not be able to continue
import logging
# By default, only WARNING and above are shown
logging.debug("This will NOT appear")
logging.info("This will NOT appear either")
logging.warning("This WILL appear")
logging.error("This WILL appear too")
logging.critical("This WILL also appear")
Configuring logging with basicConfig()¶
To see messages at all levels, including DEBUG and INFO, you need to configure the
logging system. The quickest way is with logging.basicConfig().
The level parameter controls the minimum severity of messages that will be shown.
import importlib
import logging
# Reset the logging module so basicConfig() takes effect
importlib.reload(logging)
# Configure logging to show all messages from DEBUG level upwards
logging.basicConfig(level=logging.DEBUG)
logging.debug("This is a DEBUG message")
logging.info("This is an INFO message")
logging.warning("This is a WARNING message")
Now all three messages appear, because we set the level to logging.DEBUG.
Important: logging.basicConfig() only takes effect the first time it is called.
If the logging system has already been configured (for example, by a previous call), subsequent
calls to basicConfig() have no effect. In these tutorial cells, we use importlib.reload(logging)
to reset the module so we can demonstrate different configurations.
Adding a format¶
You can also customise the format of log messages using the format parameter.
Format strings use special placeholders such as %(asctime)s for the timestamp,
%(levelname)s for the severity level, and %(message)s for the actual message.
import importlib
import logging
importlib.reload(logging)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(levelname)s - %(message)s"
)
logging.info("Application started")
logging.warning("Disk space is running low")
Creating a named logger¶
The module-level functions (logging.info(), logging.warning(), and so on) all use
the root logger. For anything beyond a simple script, it is better to create a
named logger using logging.getLogger().
Named loggers have several advantages:
- You can identify which part of your application produced each message
- You can configure different loggers independently
- Loggers form a hierarchy based on their names, using dots as separators
The standard convention is to use __name__ as the logger name, which automatically
gives the logger the name of the current module.
import importlib
import logging
importlib.reload(logging)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
# Create a named logger
logger = logging.getLogger("my_application")
logger.info("Application started")
logger.warning("Something needs attention")
Notice the %(name)s field now shows my_application in the output. This makes it
easy to trace which component produced each log message, especially in larger applications
with many modules.
Putting it all together¶
Let us write a small function that processes a list of numbers and uses logging to report what it is doing. This is a more realistic example of how logging works in practice.
import importlib
import logging
importlib.reload(logging)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("data_processor")
def process_numbers(numbers: list[int]) -> list[int]:
"""Double each number in the list, skipping negative values.
Args:
numbers: A list of integers to process.
Returns:
A list of doubled positive integers.
"""
logger.info("Starting processing of %s numbers", len(numbers))
results = []
for number in numbers:
if number < 0:
logger.warning("Skipping negative number: %s", number)
continue
doubled = number * 2
logger.debug("Doubled %s to %s", number, doubled)
results.append(doubled)
logger.info("Processing complete: %s results produced", len(results))
return results
output = process_numbers([1, 5, -3, 10, -7, 4])
print("Results:", output)
Notice several things about this example:
- We use
%splaceholders instead of f-strings in log calls. This is important because%sformatting is lazy -- the string is only formatted if the message will actually be emitted. With f-strings, the formatting always happens, even if the log level means the message will be discarded. - Different log levels convey different meanings:
INFOfor normal progress,WARNINGfor unexpected situations, andDEBUGfor detailed diagnostic information. - The
print()call at the end is for the actual program output, which is separate from the diagnostic logging.
Exercises¶
Try these exercises to reinforce what you have learned.
Exercise 1: Configure and log¶
Configure basicConfig() to show messages at INFO level and above, with a format
that includes the level name and message. Then log one message at each of the five levels.
Which messages appear?
# Exercise 1: Solution
import importlib
import logging
importlib.reload(logging)
logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
logging.debug("Debug message") # Will NOT appear (below INFO)
logging.info("Info message") # Will appear
logging.warning("Warning message") # Will appear
logging.error("Error message") # Will appear
logging.critical("Critical message") # Will appear
Exercise 2: Named logger¶
Create a named logger called "weather_app". Configure logging to include the logger
name in the output. Use the logger to log an INFO message and a WARNING message.
# Exercise 2: Solution
import importlib
import logging
importlib.reload(logging)
logging.basicConfig(
level=logging.DEBUG,
format="%(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("weather_app")
logger.info("Fetching weather data for London")
logger.warning("API rate limit is approaching: %s of %s requests used", 95, 100)
Exercise 3: Add logging to a function¶
Write a function called divide(a, b) that returns the result of dividing a by b.
Use logging to:
- Log an
INFOmessage when the function is called, showing the arguments - Log an
ERRORmessage ifbis zero - Log a
DEBUGmessage showing the result
# Exercise 3: Solution
import importlib
import logging
importlib.reload(logging)
logging.basicConfig(
level=logging.DEBUG,
format="%(levelname)s - %(message)s"
)
logger = logging.getLogger("calculator")
def divide(a: float, b: float) -> float | None:
"""Divide a by b, returning None if b is zero.
Args:
a: The numerator.
b: The denominator.
Returns:
The result of a / b, or None if b is zero.
"""
logger.info("Dividing %s by %s", a, b)
if b == 0:
logger.error("Cannot divide by zero")
return None
result = a / b
logger.debug("Result: %s", result)
return result
divide(10, 3)
divide(5, 0)
Summary¶
In this tutorial, you learned the following:
- The
loggingmodule provides structured diagnostic output with severity levels, timestamps, and configurable destinations logging.basicConfig()is the quickest way to configure logging, with parameters forlevelandformat- The five log levels, from least to most severe, are
DEBUG,INFO,WARNING,ERROR, andCRITICAL - By default, only
WARNINGand above are shown - Named loggers (created with
logging.getLogger()) help identify which part of your application produced each message - Use
%sformatting in log calls, not f-strings, for lazy evaluation
Next tutorial: Continue to Log levels and formatting to explore log levels in depth and learn how to customise the format of your log messages.