Skip to main content

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.

Featureprint()logging
Output destinationConsole onlyFiles, console, email, HTTP endpoints
Severity levelsNoneDEBUG, INFO, WARNING, ERROR, CRITICAL
MetadataManual formattingAutomatic timestamps, line numbers, module names
Runtime controlRequires code changesSingle configuration change
PerformanceAlways executesCan 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
tip

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:

LevelNumeric ValueWhen to Use
DEBUG10Detailed diagnostic information for developers
INFO20Confirmation that things work as expected
WARNING30Something unexpected happened but the app still works
ERROR40A function failed to execute properly
CRITICAL50The 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")
info

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
caution

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

SituationUse This
Temporary debugging during developmentprint()
Any code going to productionlogging
Tracking application flowlogging.info()
Recording errors with contextlogging.error()
Capturing exceptions with tracebackslogging.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.