Table of Contents
- Why Logging Matters: Beyond
printStatements - Getting Started: Basic Logging Setup
- Logging Levels: Controlling Verbosity
- Configuring Logging:
basicConfigand Beyond - Log Formatting: Adding Context to Messages
- Handlers: Routing Logs to Destinations
- Advanced Configuration:
dictConfigandfileConfig - Best Practices for Effective Logging
- Common Pitfalls to Avoid
- Complete Example: A Production-Ready Logger
- Conclusion
- References
Why Logging Matters: Beyond print Statements
At first glance, print statements and logging might seem interchangeable, but they serve distinct purposes. Here’s why logging is superior:
- Granular Control: Logging supports severity levels (e.g.,
DEBUG,INFO,ERROR), allowing you to filter messages by importance. - Structured Output: Logs can include timestamps, module names, and context (e.g., user IDs), making them easier to analyze.
- Flexible Routing: Logs can be directed to files, consoles, external services (e.g., Elasticsearch), or even emails—without changing application code.
- Scalability: The
loggingmodule is designed for large applications, with support for hierarchical loggers, rotating files, and thread safety.
In short, print is for temporary debugging; logging is for building maintainable, observable software.
Getting Started: Basic Logging Setup
The logging module is part of Python’s standard library, so no additional installation is needed. Let’s start with a simple example:
import logging
# Basic configuration
logging.basicConfig(level=logging.DEBUG)
# Log messages at different levels
logging.debug("This is a debug message") # Detailed debugging info
logging.info("This is an info message") # General runtime information
logging.warning("This is a warning message") # Unexpected but non-fatal issue
logging.error("This is an error message") # Failed operation
logging.critical("This is a critical message") # Severe error, app may fail
Output:
DEBUG:root:This is a debug message
INFO:root:This is an info message
WARNING:root:This is a warning message
ERROR:root:This is an error message
CRITICAL:root:This is a critical message
Here, basicConfig configures the root logger (the default logger if no named logger is specified). The level=logging.DEBUG parameter ensures all messages at DEBUG level and above are emitted.
Logging Levels: Controlling Verbosity
The logging module defines five standard severity levels (in increasing order of severity):
| Level | Numeric Value | Use Case |
|---|---|---|
DEBUG | 10 | Detailed information for debugging (e.g., variable values, function calls). |
INFO | 20 | Confirmation that things are working as expected (e.g., “Server started”). |
WARNING | 30 | An indication of an unexpected condition that might cause issues later (e.g., “Low disk space”). |
ERROR | 40 | Due to a more serious problem, the software has not been able to perform an action (e.g., “Failed to connect to database”). |
CRITICAL | 50 | A serious error indicating the program itself may be unable to continue running (e.g., “Database connection failed; exiting”). |
Key Behavior: A logger will emit messages at or above its configured level. For example:
- If a logger is set to
INFO, it will emitINFO,WARNING,ERROR, andCRITICALmessages (but notDEBUG). - If set to
ERROR, onlyERRORandCRITICALmessages are emitted.
This allows you to adjust verbosity dynamically (e.g., DEBUG for development, WARNING for production).
Configuring Logging: basicConfig and Beyond
The basicConfig function is the simplest way to configure logging. It accepts parameters to customize output:
| Parameter | Purpose |
|---|---|
level | Set the logging threshold (e.g., logging.INFO). |
format | A string specifying the log message format (see Log Formatting). |
datefmt | Format for timestamps (e.g., "%Y-%m-%d %H:%M:%S"). |
filename | Path to a file to write logs to (instead of the console). |
filemode | File mode for filename ("w" for overwrite, "a" for append; default: "a"). |
handlers | A list of handlers to add to the root logger (advanced). |
Example: Logging to a File
import logging
logging.basicConfig(
level=logging.INFO,
filename="app.log",
filemode="a", # Append to the file (default)
format="%(asctime)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
logging.info("Application started")
logging.warning("Low memory detected")
This writes logs to app.log with timestamps:
2024-05-20 14:30:00 - INFO - Application started
2024-05-20 14:30:05 - WARNING - Low memory detected
Log Formatting: Adding Context to Messages
By default, logs include the level and message, but you can enrich them with context like timestamps, logger names, or module paths using format specifiers. Common format attributes include:
| Specifier | Description |
|---|---|
%(asctime)s | Human-readable timestamp (configurable via datefmt). |
%(levelname)s | Log level (e.g., “DEBUG”, “INFO”). |
%(message)s | The log message itself. |
%(name)s | Name of the logger (e.g., “myapp.module”). |
%(module)s | Module where the log call occurred. |
%(lineno)d | Line number in the module where the log call occurred. |
%(process)d | Process ID (useful for multi-process applications). |
%(threadName)s | Thread name (useful for multi-threaded applications). |
Example: Custom Format
import logging
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
logging.debug("User 'alice' logged in")
Output:
2024-05-20 14:35:00 - root - DEBUG - my_script:8 - User 'alice' logged in
This format includes the timestamp, logger name (root here), level, module/line number, and message—making it easy to trace issues.
Handlers: Routing Logs to Destinations
Handlers determine where log messages are sent (e.g., console, file, email). The logging module includes several built-in handlers; here are the most useful:
1. StreamHandler: Log to Console
Sends logs to a stream (default: sys.stderr, but can be sys.stdout). Used by default if no filename is specified in basicConfig.
2. FileHandler: Log to a File
Writes logs to a specified file. Similar to using filename in basicConfig, but more flexible for advanced setups.
3. RotatingFileHandler: Rotate Logs by Size
Prevents log files from growing indefinitely by rotating them when they reach a specified size. Requires from logging.handlers import RotatingFileHandler.
4. TimedRotatingFileHandler: Rotate Logs by Time
Rotates logs at fixed intervals (e.g., daily, hourly). Useful for retaining historical logs. Requires from logging.handlers import TimedRotatingFileHandler.
Example: Multiple Handlers (Console + Rotating File)
import logging
from logging.handlers import RotatingFileHandler
# Create a logger (use __name__ for module-specific logging)
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) # Capture all levels at the logger level
# Define formatters
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
# Console handler (logs INFO+ to console)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO) # Only INFO+ to console
console_handler.setFormatter(formatter)
# Rotating file handler (logs DEBUG+ to file, max 5MB per file, keep 3 backups)
file_handler = RotatingFileHandler(
"app.log",
maxBytes=5*1024*1024, # 5MB
backupCount=3, # Keep 3 backup logs
encoding="utf-8"
)
file_handler.setLevel(logging.DEBUG) # DEBUG+ to file
file_handler.setFormatter(formatter)
# Add handlers to the logger
logger.addHandler(console_handler)
logger.addHandler(file_handler)
# Test the logger
logger.debug("Debug message (file only)")
logger.info("Info message (console and file)")
logger.error("Error message (console and file)")
Key Points:
- Handlers can have their own log levels (e.g.,
console_handlerignoresDEBUG). - Multiple handlers allow logs to be routed to multiple destinations simultaneously.
RotatingFileHandlerensures logs don’t consume excessive disk space.
Advanced Configuration: dictConfig and fileConfig
For complex applications (e.g., microservices with multiple loggers/handlers), basicConfig becomes unwieldy. Instead, use dictConfig (recommended) or fileConfig to define configurations in a structured format.
Example: dictConfig
import logging
from logging.config import dictConfig
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False, # Preserve existing loggers
"formatters": {
"detailed": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S"
},
"simple": {
"format": "%(levelname)s - %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "simple"
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "DEBUG",
"formatter": "detailed",
"filename": "app.log",
"maxBytes": 5*1024*1024,
"backupCount": 3,
"encoding": "utf-8"
}
},
"loggers": {
"myapp": { # Named logger for "myapp" module
"handlers": ["console", "file"],
"level": "DEBUG",
"propagate": False # Don't propagate to root logger
}
},
"root": { # Root logger (fallback)
"handlers": ["console"],
"level": "WARNING"
}
}
# Apply the configuration
dictConfig(LOGGING_CONFIG)
# Use the named logger
logger = logging.getLogger("myapp")
logger.debug("Debug message from myapp")
logger.info("Info message from myapp")
dictConfig is powerful because configurations can be loaded from JSON/YAML files, enabling environment-specific setups (e.g., development vs. production).
Best Practices for Effective Logging
1. Use Named Loggers
Avoid using the root logger directly. Instead, create named loggers with logging.getLogger(__name__), where __name__ is the module’s dotted path (e.g., myapp.utils). This ensures:
- Logs are traceable to their source module.
- Loggers can be configured hierarchically (e.g.,
myapplogger applies to all submodules likemyapp.utils).
# In myapp/utils.py
logger = logging.getLogger(__name__) # Name: "myapp.utils"
logger.info("Utility function called")
2. Include Context in Logs
Logs are most useful when they include context about the runtime environment. Examples:
- User IDs, session IDs, or request IDs for web apps.
- Timestamps (always include these!).
- Input parameters or state information relevant to the log message.
user_id = "alice123"
logger.info(f"User {user_id} completed checkout", extra={"user_id": user_id}) # `extra` adds structured context
3. Log Exceptions Properly
Use logger.exception within except blocks to automatically include the traceback:
try:
result = 1 / 0
except ZeroDivisionError:
logger.exception("Failed to compute result") # Includes traceback
Output includes the error message and full stack trace, critical for debugging.
4. Avoid Sensitive Information
Never log passwords, API keys, or PII (Personally Identifiable Information). Use redaction if necessary:
# Bad: logger.debug(f"User login: {username}, password: {password}")
logger.debug(f"User login attempt: {username}") # Omit password
5. Use Structured Logs
For machine-readable logs (e.g., for analysis with tools like Splunk or ELK Stack), format logs as JSON:
import json
from pythonjsonlogger import jsonlogger # Install with `pip install python-json-logger`
formatter = jsonlogger.JsonFormatter(
"%(asctime)s %(levelname)s %(name)s %(message)s %(user_id)s"
)
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.info("User login", extra={"user_id": "alice123"})
Output (JSON):
{"asctime": "2024-05-20 15:00:00", "levelname": "INFO", "name": "myapp", "message": "User login", "user_id": "alice123"}
6. Rotate Logs
Use RotatingFileHandler or TimedRotatingFileHandler to prevent log files from consuming all disk space.
Common Pitfalls to Avoid
1. Forgetting to Configure Logging
If you don’t configure logging, the root logger defaults to WARNING level and no handlers, so DEBUG/INFO messages are silently dropped. Always configure logging explicitly.
2. Overusing the Root Logger
The root logger is shared across all modules. Using it directly can lead to unintended side effects if other libraries configure it. Use named loggers instead.
3. Logging the Same Message Multiple Times
If multiple handlers are added to a logger and its parent (e.g., root logger), messages may be duplicated. Use propagate=False in child loggers to prevent this.
4. Ignoring Log Levels in Production
In production, avoid logging DEBUG messages—they clutter logs and may expose sensitive details. Use INFO or WARNING as the default level.
5. Using print Instead of Logging
print statements are not configurable, can’t be routed to files, and lack severity levels. Replace them with logging calls.
Complete Example: A Production-Ready Logger
Here’s a full example combining best practices: named loggers, rotating files, JSON formatting, and context-rich messages.
import logging
import json
from logging.handlers import TimedRotatingFileHandler
from pythonjsonlogger import jsonlogger
def setup_logger(name=__name__):
"""Configure a production-ready logger with JSON formatting and daily rotation."""
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
logger.propagate = False # Prevent propagation to root
# JSON formatter with context
json_formatter = jsonlogger.JsonFormatter(
"%(asctime)s %(levelname)s %(name)s %(message)s %(user_id)s %(request_id)s",
rename_fields={"levelname": "level", "asctime": "timestamp"}
)
# Daily rotating file handler (keep 7 days of logs)
file_handler = TimedRotatingFileHandler(
"app.log",
when="midnight",
interval=1,
backupCount=7,
encoding="utf-8"
)
file_handler.setFormatter(json_formatter)
logger.addHandler(file_handler)
return logger
# Usage
logger = setup_logger()
# Log with context
logger.info(
"User checkout successful",
extra={"user_id": "alice123", "request_id": "req-456"}
)
# Log an exception
try:
1 / 0
except ZeroDivisionError:
logger.exception(
"Checkout failed",
extra={"user_id": "alice123", "request_id": "req-456"}
)
This logger produces JSON logs like:
{"timestamp": "2024-05-20 15:30:00", "level": "INFO", "name": "myapp", "message": "User checkout successful", "user_id": "alice123", "request_id": "req-456"}
Conclusion
Effective logging is a cornerstone of maintainable, observable software. Python’s logging module provides a flexible, scalable framework to generate structured, context-rich logs with minimal effort. By following best practices—using named loggers, including context, configuring handlers appropriately, and avoiding common pitfalls—you can unlock powerful insights into your application’s behavior, simplify debugging, and ensure reliability in production.
Start small with basicConfig, then graduate to dictConfig and advanced handlers as your application grows. The time invested in proper logging will pay dividends when troubleshooting issues in production.
References
- Python Official Logging Documentation
- Python Logging Cookbook
- Structured Logging with Python (alternative to
python-json-logger) - Logging Best Practices (Datadog)
This blog provides a comprehensive guide to mastering Python’s `logging` module, from basics to advanced use cases. By implementing these techniques, you’ll create logs that are actionable, scalable, and invaluable for maintaining robust applications.