How to Replace print() with Logging in Python
When transitioning from quick scripts to production-ready applications, understanding when and how to replace print() statements with proper logging becomes essential.
This guide walks you through the process and explains why this change matters for maintainable code.
Why Move Beyond print()
While print() works perfectly for quick debugging during development, it lacks the flexibility needed for production environments where you need control over output destinations, severity levels, and message formatting.
| Feature | print() | logging |
|---|---|---|
| Output destination | Console only | Files, console, email, HTTP endpoints |
| Severity levels | None | DEBUG, INFO, WARNING, ERROR, CRITICAL |
| Metadata | Manual formatting | Automatic timestamps, line numbers, module names |
| Runtime control | Requires code changes | Single configuration change |
| Performance | Always executes | Can disable by level |
Setting Up Basic Logging
The simplest way to start using logging requires just two lines of code:
import logging
logging.basicConfig(level=logging.INFO)
logging.info("Application started successfully")
logging.warning("Configuration file not found, using defaults")
logging.error("Failed to connect to database")
Output:
INFO:root:Application started successfully
WARNING:root:Configuration file not found, using defaults
ERROR:root:Failed to connect to database
Set the logging level to DEBUG during development and INFO or WARNING in production to control verbosity without changing your code.
Configuring File Output
Production applications typically write logs to files for later analysis:
import logging
logging.basicConfig(
filename='application.log',
format='%(asctime)s - %(levelname)s - %(message)s',
level=logging.DEBUG,
datefmt='%Y-%m-%d %H:%M:%S'
)
logging.debug("Processing user request")
logging.info("User authentication successful")
logging.warning("API rate limit approaching")
logging.error("Payment processing failed")
logging.critical("Database connection lost")
File content:
2025-01-15 14:32:01 - DEBUG - Processing user request
2025-01-15 14:32:01 - INFO - User authentication successful
2025-01-15 14:32:02 - WARNING - API rate limit approaching
2025-01-15 14:32:02 - ERROR - Payment processing failed
2025-01-15 14:32:03 - CRITICAL - Database connection lost
Understanding Logging Levels
Each level serves a specific purpose in categorizing your messages:
| Level | Numeric Value | When to Use |
|---|---|---|
DEBUG | 10 | Detailed diagnostic information for developers |
INFO | 20 | Confirmation that things work as expected |
WARNING | 30 | Something unexpected happened but the app still works |
ERROR | 40 | A function failed to execute properly |
CRITICAL | 50 | The application cannot continue running |
import logging
logging.basicConfig(level=logging.WARNING)
logging.debug("This won't appear") # Below threshold
logging.info("This won't appear") # Below threshold
logging.warning("This will appear") # Meets threshold
logging.error("This will appear") # Above threshold
Output:
WARNING:root:This will appear
ERROR:root:This will appear
Creating Custom Loggers
For larger applications, create dedicated loggers for different modules:
import logging
# Create a custom logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# Create handlers
console_handler = logging.StreamHandler()
file_handler = logging.FileHandler('debug.log')
# Set levels for handlers
console_handler.setLevel(logging.WARNING)
file_handler.setLevel(logging.DEBUG)
# Create formatters
console_format = logging.Formatter('%(levelname)s - %(message)s')
file_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# Attach formatters to handlers
console_handler.setFormatter(console_format)
file_handler.setFormatter(file_format)
# Add handlers to logger
logger.addHandler(console_handler)
logger.addHandler(file_handler)
# Use the logger
logger.debug("Detailed info for file only")
logger.warning("Warning shown in console and file")
Using __name__ as the logger name automatically creates a hierarchy based on your module structure, making it easier to configure logging for specific parts of your application.
Logging Exceptions
Capture full stack traces when exceptions occur:
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
logging.exception("Division by zero attempted")
return None
result = divide(10, 0)
Output:
2025-01-15 14:35:22 - ERROR - Division by zero attempted
Traceback (most recent call last):
File "example.py", line 10, in divide
return a / b
ZeroDivisionError: division by zero
Use logging.exception() only inside exception handlers. It automatically includes the traceback information at ERROR level.
Migrating from print() to logging
Here's a practical before-and-after comparison:
Before (using print)
def process_order(order_id):
print(f"Processing order {order_id}")
if not validate_order(order_id):
print(f"ERROR: Order {order_id} is invalid")
return False
print(f"Order {order_id} validated successfully")
return True
After (using logging)
import logging
logger = logging.getLogger(__name__)
def process_order(order_id):
logger.info(f"Processing order {order_id}")
if not validate_order(order_id):
logger.error(f"Order {order_id} is invalid")
return False
logger.debug(f"Order {order_id} validated successfully")
return True
Quick Reference
| Situation | Use This |
|---|---|
| Temporary debugging during development | print() |
| Any code going to production | logging |
| Tracking application flow | logging.info() |
| Recording errors with context | logging.error() |
| Capturing exceptions with tracebacks | logging.exception() |
Making the switch from print() to logging is an important step in writing professional, maintainable Python code. The initial setup takes only a few minutes, but the benefits in debugging, monitoring, and maintaining production applications are substantial.