🕯️ Magic Note
The logging module is one of Python’s most underrated features. It is thread-safe, highly configurable, and can handle millions of log messages efficiently. Most professional Python applications use it exclusively, not print().
| Level | Numeric Value | When to Use |
|---|---|---|
| DEBUG | 10 | Detailed information for diagnosing problems (development only) |
| INFO | 20 | Confirmation that things are working as expected |
| WARNING | 30 | Something unexpected happened, but the program is still working |
| ERROR | 40 | A more serious problem, the program cannot perform a specific function |
| CRITICAL | 50 | A serious error, the program may not be able to continue running |
Python
import logging
# Basic configuration
logging.basicConfig(level=logging.DEBUG)
# Log messages at different levels
logging.debug(“Detailed debugging information”)
logging.info(“Informational message”)
logging.warning(“Warning: something unexpected”)
logging.error(“Error: something failed”)
logging.critical(“Critical: program may crash”)
# Output (depending on the level configured):
# DEBUG:root:Detailed debugging information
# INFO:root:Informational message
# WARNING:root:Warning: something unexpected
# ERROR:root:Error: something failed
# CRITICAL:root:Critical: program may crash
Python
import logging
# Log to file instead of console
logging.basicConfig(filename=”app.log”, level=logging.INFO)
# Log with custom format (timestamp, level, message)
logging.basicConfig(
level=logging.DEBUG,
format=”%(asctime)s – %(name)s – %(levelname)s – %(message)s”,
datefmt=”%Y-%m-%d %H:%M:%S”
)
logging.info(“This has a timestamp!”)
# Output: 2025-05-11 14:30:45 – root – INFO – This has a timestamp!
🕯️ Magic Note
The basicConfig() function only works the first time it is called. After that, changes have no effect. Call it once at the beginning of your program, before any logging occurs.
| Format Specifier | Meaning |
|---|---|
| %(asctime)s | Human-readable time (when the log was created) |
| %(name)s | Name of the logger (usually the module name) |
| %(levelname)s | Logging level (DEBUG, INFO, WARNING, etc.) |
| %(message)s | The log message itself |
| %(filename)s | Source file name |
| %(lineno)d | Line number in the source file |
| %(funcName)s | Function name where the log was called |
| %(pathname)s | Full path of the source file |
| %(process)d | Process ID |
| %(thread)d | Thread ID |
| %(created)f | Time when the log was created (as float) |
Python
import logging
logging.basicConfig(
level=logging.DEBUG,
format=”[%(asctime)s] [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s”,
datefmt=”%H:%M:%S”
)
def calculate(x, y):
logging.debug(f”Calculating {x} + {y}”)
return x + y
calculate(5, 3)
# Output: [14:30:45] [DEBUG] [script.py:8] Calculating 5 + 3
Python
import logging
# Create a logger for this module
logger = logging.getLogger(__name__)
# Configure (can be configured once at the top level)
logging.basicConfig(level=logging.DEBUG, format=”%(asctime)s – %(name)s – %(levelname)s – %(message)s”)
# Use the logger (not the root logger)
logger.debug(“This is a debug message from a named logger”)
logger.info(“This is an info message”)
logger.warning(“This is a warning”)
logger.error(“This is an error”)
# The name helps identify where the log came from
# 2025-05-11 14:30:45 – __main__ – DEBUG – This is a debug message
🕯️ Magic Note
Using logger = logging.getLogger(__name__) is the standard pattern. The logger name will be the module’s dotted path (e.g., “my_package.submodule”), making it easy to identify the source of each log message.
Python
import logging
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
# Remove the default configuration
logging.root.handlers = []
# Create a custom logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# Rotating file handler: keep files under 1MB, keep 5 backups
file_handler = RotatingFileHandler(“app.log”, maxBytes=1_000_000, backupCount=5)
# Or time-based rotation: rotate daily, keep 7 days
# file_handler = TimedRotatingFileHandler(“app.log”, when=”midnight”, interval=1, backupCount=7)
# Set formatter
formatter = logging.Formatter(“%(asctime)s – %(name)s – %(levelname)s – %(message)s”)
file_handler.setFormatter(formatter)
# Add handler to logger
logger.addHandler(file_handler)
# Also log to console
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
logger.info(“This goes to both file and console”)
logger.debug(“This only appears in the file (since console handler is INFO)”)
Python
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) # Master level (all handlers inherit this)
# File handler: log everything (DEBUG and above)
file_handler = logging.FileHandler(“app.log”)
file_handler.setLevel(logging.DEBUG)
# Console handler: only warnings and above
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.WARNING)
# Formatter for both
formatter = logging.Formatter(“%(asctime)s – %(levelname)s – %(message)s”)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logger.addHandler(console_handler)
logger.debug(“This goes to file only”)
logger.info(“This goes to file only”)
logger.warning(“This goes to both file and console”)
logger.error(“This goes to both file and console”)
Python
import logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.ERROR, format=”%(asctime)s – %(levelname)s – %(message)s”)
try:
result = 10 / 0
except ZeroDivisionError as e:
# Log the exception with full traceback
logger.error(“An error occurred”, exc_info=True)
# Or use exception() which is equivalent to error(…, exc_info=True)
try:
result = 10 / 0
except ZeroDivisionError as e:
logger.exception(“An error occurred”) # Automatically includes traceback
🕯️ Magic Note
Use logger.exception() inside exception handlers. It logs at ERROR level and automatically includes the full traceback. Your debugging will be much easier.
Python
import logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO, format=”%(asctime)s – %(levelname)s – %(message)s”)
name = “Feloriya”
score = 95
# Using f-strings (works but format string is evaluated even if log is disabled)
logger.info(f”User {name} scored {score}”)
# Better: use lazy formatting (format string only evaluated if level is enabled)
logger.info(“User %s scored %d”, name, score)
# Or using .format() style
logger.info(“User {} scored {}”.format(name, score))
INI (logging.conf)
[loggers]
keys=root,myapp
[handlers]
keys=consoleHandler,fileHandler
[formatters]
keys=simpleFormatter
[logger_root]
level=WARNING
handlers=consoleHandler
[logger_myapp]
level=DEBUG
handlers=consoleHandler,fileHandler
qualname=myapp
propagate=0
[handler_consoleHandler]
class=StreamHandler
level=INFO
formatter=simpleFormatter
args=(sys.stdout,)
[handler_fileHandler]
class=handlers.RotatingFileHandler
level=DEBUG
formatter=simpleFormatter
args=(“app.log”, “a”, 1000000, 5)
[formatter_simpleFormatter]
format=%(asctime)s – %(name)s – %(levelname)s – %(message)s
datefmt=%Y-%m-%d %H:%M:%S
Python
import logging.config
# Load configuration from file
logging.config.fileConfig(“logging.conf”)
# Get a logger for your application
logger = logging.getLogger(“myapp”)
logger.debug(“This will go to the file only”)
logger.info(“This will go to both file and console”)
logger.warning(“This is a warning”)
Python
import logging
from logging.handlers import RotatingFileHandler
import os
def setup_logging(app):
“””Configure logging for a Flask application.”””
# Create logs directory if it doesn’t exist
if not os.path.exists(“logs”):
os.makedirs(“logs”)
# Remove default handlers
app.logger.handlers = []
# Set log level based on environment
env = os.environ.get(“FLASK_ENV”, “production”)
if env == “development”:
log_level = logging.DEBUG
else:
log_level = logging.INFO
# File handler (all logs)
file_handler = RotatingFileHandler(“logs/app.log”, maxBytes=10_000_000, backupCount=10)
file_handler.setLevel(logging.DEBUG)
# Error file handler (only errors)
error_handler = RotatingFileHandler(“logs/error.log”, maxBytes=10_000_000, backupCount=10)
error_handler.setLevel(logging.ERROR)
# Console handler (for development)
console_handler = logging.StreamHandler()
console_handler.setLevel(log_level)
# Formatter
formatter = logging.Formatter(
“[%(asctime)s] [%(levelname)s] [%(name)s] [%(filename)s:%(lineno)d] %(message)s”
)
file_handler.setFormatter(formatter)
error_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# Add handlers
app.logger.addHandler(file_handler)
app.logger.addHandler(error_handler)
app.logger.addHandler(console_handler)
app.logger.setLevel(logging.DEBUG)
app.logger.info(“Logging configured successfully”)
| Use print() When... | Use logging When... |
|---|---|
| Quick debugging in a script | Production applications |
| One-off scripts you will delete | Long-running services |
| Learning / experimenting | Applications deployed on servers |
| You need to see output immediately | You need to review logs later |
| Simple console output only | You need file output, rotation, timestamps |
| No need for severity levels | You need error levels (DEBUG, INFO, ERROR) |
- Using print() instead of logging in production code
- Calling basicConfig() after logging has already occurred
- Using f-strings in log messages (formatting happens even if log is disabled)
- Not setting up log rotation (files grow indefinitely)
- Using the root logger directly instead of named loggers
- Forgetting to configure logging before importing other modules
- What are the five logging levels in Python?
- How do you create a named logger for the current module?
- Write code that logs DEBUG to a file and INFO to the console.
- How do you log an exception with full traceback?
- Why should you avoid f-strings in log messages?
- What is the purpose of RotatingFileHandler?
⚡ Whisper
Print is for the terminal. Logging is for the world. Print shouts into the void. Logging whispers to a file, a timestamp, a level. When your program runs on a server at 3 AM and crashes, print is silent. Logging tells you what happened. DEBUG for details. INFO for progress. WARNING for surprises. ERROR for failures. CRITICAL for disasters. Each level has a purpose. Each message has a format. Timestamps, filenames, line numbers. You can search them. You can filter them. You can rotate them. Logging is not complicated. It is basicConfig() and getLogger(__name__). It is logger.info() instead of print(). Start today. Replace your prints. Your future self will thank you. The server logs will tell the story. Read it.