Log levels and formatting¶
In this tutorial, you will explore the five standard log levels in depth and learn how to customise the format of your log messages. Understanding log levels is essential for controlling which messages appear in your output.
Time commitment: 15–20 minutes
Prerequisites:
- Your first log message tutorial completed
- Basic understanding of
logging.basicConfig()andlogging.getLogger()
Learning objectives¶
By the end of this tutorial, you will be able to:
- Name the five standard log levels and their numeric values
- Set the effective log level for a logger
- Use format strings to customise log message appearance
- Apply date formatting with the
datefmtparameter - Explain why
%sformatting is preferred over f-strings in log calls
The five standard log levels¶
The logging module defines five standard severity levels, each with a name and a
numeric value. The numeric values determine the ordering: higher values represent
more severe events.
| Level | Numeric value | Purpose |
|---|---|---|
DEBUG |
10 | Detailed diagnostic information for developers |
INFO |
20 | Confirmation that things are working as expected |
WARNING |
30 | Something unexpected happened, but the program still works |
ERROR |
40 | A specific operation failed |
CRITICAL |
50 | The program itself may not be able to continue |
import logging
# Each level has a numeric value
print("DEBUG:", logging.DEBUG)
print("INFO:", logging.INFO)
print("WARNING:", logging.WARNING)
print("ERROR:", logging.ERROR)
print("CRITICAL:", logging.CRITICAL)
How log levels work¶
When you set a log level on a logger, only messages at that level or above are processed. Messages below the effective level are silently discarded.
For example, if you set the level to WARNING (30), then:
DEBUG(10) messages are discardedINFO(20) messages are discardedWARNING(30) messages are processedERROR(40) messages are processedCRITICAL(50) messages are processed
Think of it as a threshold: only messages that meet or exceed the threshold pass through.
import importlib
import logging
importlib.reload(logging)
# Set level to INFO – DEBUG messages will be suppressed
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logging.debug("This will NOT appear (below INFO threshold)")
logging.info("This WILL appear")
logging.warning("This WILL appear")
logging.error("This WILL appear")
Setting the log level¶
There are two main ways to set the log level:
- Using
logging.basicConfig(level=...)-- Sets the level on the root logger - Using
logger.setLevel()-- Sets the level on a specific named logger
Let us see both approaches.
import importlib
import logging
importlib.reload(logging)
logging.basicConfig(format="%(name)s - %(levelname)s: %(message)s")
# Create two loggers with different levels
app_logger = logging.getLogger("app")
app_logger.setLevel(logging.DEBUG)
db_logger = logging.getLogger("database")
db_logger.setLevel(logging.WARNING)
# The app logger shows everything from DEBUG upwards
app_logger.debug("Detailed app information")
app_logger.info("App is running")
# The database logger only shows WARNING and above
db_logger.debug("This will NOT appear")
db_logger.info("This will NOT appear either")
db_logger.warning("Connection pool is almost full")
Formatting log messages¶
The format of log messages is controlled by format strings. These strings use special placeholders that are replaced with information from the log record when a message is emitted.
Format placeholders use the %(name)s syntax, where name is an attribute of the
log record.
Common format attributes¶
The following table lists the most commonly used format attributes:
| Attribute | Format | Description |
|---|---|---|
asctime |
%(asctime)s |
Human-readable timestamp |
name |
%(name)s |
Name of the logger |
levelname |
%(levelname)s |
Severity level (DEBUG, INFO, and so on) |
message |
%(message)s |
The log message |
filename |
%(filename)s |
Name of the source file |
lineno |
%(lineno)d |
Line number in the source file |
funcName |
%(funcName)s |
Name of the function that logged the message |
import importlib
import logging
importlib.reload(logging)
# A detailed format string with timestamp, logger name, level, and message
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("my_app")
logger.info("Application started")
logger.debug("Loading configuration")
logger.warning("Configuration file not found, using defaults")
import importlib
import logging
importlib.reload(logging)
# Include source file location in the format
logging.basicConfig(
level=logging.DEBUG,
format="%(levelname)s [%(filename)s:%(lineno)d] %(message)s"
)
logger = logging.getLogger("location_demo")
logger.info("This message includes source location")
Date formatting¶
By default, the %(asctime)s attribute produces timestamps in the format
2024-01-15 14:30:00,123. You can customise this using the datefmt parameter,
which accepts standard strftime directives.
Some useful directives:
| Directive | Meaning | Example |
|---|---|---|
%d |
Day of the month (zero-padded) | 09 |
%m |
Month (zero-padded) | 02 |
%Y |
Four-digit year | 2026 |
%H |
Hour (24-hour, zero-padded) | 14 |
%M |
Minute (zero-padded) | 30 |
%S |
Second (zero-padded) | 05 |
import importlib
import logging
importlib.reload(logging)
# Use a British date format: DD/MM/YYYY HH:MM:SS
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(levelname)s - %(message)s",
datefmt="%d/%m/%Y %H:%M:%S"
)
logger = logging.getLogger("date_demo")
logger.info("This message has a custom date format")
Lazy string formatting¶
An important best practice when using the logging module is to use %s style
formatting in log calls instead of f-strings.
Why? With %s formatting, the string is only formatted if the message will actually
be emitted. If the log level means the message will be discarded, the formatting
is skipped entirely. This is called lazy evaluation.
With f-strings, the formatting always happens, even when the message is discarded. This wastes processing time unnecessarily.
import importlib
import logging
importlib.reload(logging)
logging.basicConfig(level=logging.WARNING, format="%(levelname)s: %(message)s")
logger = logging.getLogger("formatting_demo")
username = "alice"
action = "logged in"
# Correct: %s formatting (lazy – only formatted if DEBUG is enabled)
logger.debug("User %s has %s", username, action)
# Incorrect: f-string (always formatted, even though DEBUG is disabled)
# logger.debug(f"User {username} has {action}") # Do not do this
print("The DEBUG message above was discarded without formatting the string.")
print("With an f-string, the formatting would have happened anyway.")
Exercises¶
Try these exercises to reinforce what you have learned.
Exercise 1: Level filtering¶
Configure logging at ERROR level. Then log a message at each of the five levels.
Predict which messages will appear before running the cell.
# Exercise 1: Solution
import importlib
import logging
importlib.reload(logging)
logging.basicConfig(level=logging.ERROR, format="%(levelname)s: %(message)s")
logging.debug("Debug message") # Will NOT appear
logging.info("Info message") # Will NOT appear
logging.warning("Warning message") # Will NOT appear
logging.error("Error message") # Will appear
logging.critical("Critical message") # Will appear
# Only ERROR (40) and CRITICAL (50) meet the ERROR (40) threshold
Exercise 2: Custom format¶
Create a format string that produces output like the following:
[INFO] my_app :: Application started (line 5)
The format should include the level name in square brackets, the logger name, the message, and the line number.
# Exercise 2: Solution
import importlib
import logging
importlib.reload(logging)
logging.basicConfig(
level=logging.DEBUG,
format="[%(levelname)s] %(name)s :: %(message)s (line %(lineno)d)"
)
logger = logging.getLogger("my_app")
logger.info("Application started")
Exercise 3: Date formatting¶
Configure logging with a format that includes a British-style timestamp
(DD/MM/YYYY HH:MM:SS), the level name, and the message. Log a few test messages
to verify the output.
# Exercise 3: Solution
import importlib
import logging
importlib.reload(logging)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%d/%m/%Y %H:%M:%S"
)
logger = logging.getLogger("date_exercise")
logger.info("Server started")
logger.warning("High memory usage detected")
logger.error("Connection to database failed")
Summary¶
In this tutorial, you learned the following:
- The five standard log levels are
DEBUG(10),INFO(20),WARNING(30),ERROR(40), andCRITICAL(50) - Setting a log level creates a threshold: only messages at that level or above are processed
- Format strings use
%(attribute)splaceholders to include information such as timestamps, logger names, level names, and source locations - The
datefmtparameter customises the timestamp format usingstrftimedirectives - Always use
%sformatting in log calls, not f-strings, to benefit from lazy evaluation
Next tutorial: Continue to Logging to files to learn how to direct log output to files and combine multiple output destinations.