Python logging Module: A Complete Guide
Introduction
Most beginners use print() to debug their programs. This works for small scripts, but it falls apart for anything larger:
- You cannot easily turn debug output on or off
- You cannot route output to a file without changing the code
- You lose information about when, where, and how severe an event was
- You cannot filter by severity
Python’s logging module solves all of these problems. It is part of the standard library, requires no installation, and is used by virtually every professional Python project.
All examples are tested on Python 3.12.
The Basics
import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug("Debug message")
logging.info("Info message")
logging.warning("Warning message")
logging.error("Error message")
logging.critical("Critical message")
Expected output:
DEBUG:root:Debug message
INFO:root:Info message
WARNING:root:Warning message
ERROR:root:Error message
CRITICAL:root:Critical message
Each call corresponds to a severity level. The level=logging.DEBUG in basicConfig means all messages at DEBUG level and above will be shown.
Log Levels
Python defines five standard log levels, in increasing order of severity:
| Level | Value | When to Use |
|---|---|---|
DEBUG | 10 | Detailed information for diagnosing problems |
INFO | 20 | Confirmation that things are working as expected |
WARNING | 30 | Something unexpected happened, but the program still works |
ERROR | 40 | A serious problem — the program could not do something |
CRITICAL | 50 | A very serious error — the program may not be able to continue |
The default level is WARNING, which means DEBUG and INFO messages are hidden unless you change it:
import logging
# Default — only WARNING and above are shown
logging.warning("This appears")
logging.info("This is hidden")
# Change to INFO
logging.basicConfig(level=logging.INFO)
logging.info("Now this appears")
Using a Logger
Instead of calling logging directly (which uses the root logger), create a named logger for each module:
import logging
logger = logging.getLogger(__name__)
logger.debug("Processing started")
logger.info("Loaded 142 records")
logger.warning("Missing field: email")
logger.error("Failed to connect to database")
__name__ gives the logger the same name as the module (e.g. myapp.database). This lets you configure different loggers differently and see exactly where messages come from.
Formatting Log Messages
Control the format with basicConfig:
import logging
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s — %(name)s — %(levelname)s — %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger(__name__)
logger.info("Application started")
logger.warning("Low disk space")
Expected output:
2026-05-17 14:30:00 — __main__ — INFO — Application started
2026-05-17 14:30:00 — __main__ — WARNING — Low disk space
Common format variables:
| Variable | Content |
|---|---|
%(asctime)s | Timestamp |
%(name)s | Logger name |
%(levelname)s | Level (DEBUG, INFO, etc.) |
%(message)s | The log message |
%(filename)s | Source file name |
%(lineno)d | Line number |
%(funcName)s | Function name |
Logging to a File
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s — %(levelname)s — %(message)s",
handlers=[
logging.FileHandler("app.log"),
logging.StreamHandler(), # also print to console
]
)
logger = logging.getLogger(__name__)
logger.info("Script started")
logger.error("Something went wrong")
FileHandler writes to a file. StreamHandler writes to the console (stdout/stderr). You can combine multiple handlers to send logs to multiple destinations simultaneously.
Log Rotation
For long-running applications, log files grow indefinitely. Use RotatingFileHandler to automatically create new files when the current one gets too large:
import logging
from logging.handlers import RotatingFileHandler
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
handler = RotatingFileHandler(
"app.log",
maxBytes=1_000_000, # 1 MB per file
backupCount=5, # keep last 5 files
)
handler.setFormatter(logging.Formatter("%(asctime)s — %(levelname)s — %(message)s"))
logger.addHandler(handler)
logger.info("Using rotating log files")
When app.log reaches 1 MB, it is renamed to app.log.1, and a new app.log is created. Up to 5 backup files are kept.
Logging Exceptions
Use logger.exception() inside an except block to automatically include the full traceback:
import logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.ERROR)
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
logger.exception("Division failed")
return None
result = divide(10, 0)
Expected output:
ERROR:__main__:Division failed
Traceback (most recent call last):
File "script.py", line 8, in divide
return a / b
ZeroDivisionError: division by zero
logger.exception() is equivalent to logger.error() but also appends the current exception traceback. Always use it inside except blocks when you want to record the full error.
A Complete Logging Setup
For real applications, configure logging once at startup and use named loggers everywhere else:
# logging_config.py
import logging
import logging.config
from pathlib import Path
def setup_logging(log_level: str = "INFO", log_file: str = "app.log") -> None:
Path("logs").mkdir(exist_ok=True)
config = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"detailed": {
"format": "%(asctime)s — %(name)s — %(levelname)s — %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
"simple": {
"format": "%(levelname)s — %(message)s",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "simple",
"level": "INFO",
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"filename": f"logs/{log_file}",
"maxBytes": 5_000_000,
"backupCount": 3,
"formatter": "detailed",
"level": "DEBUG",
},
},
"root": {
"level": log_level,
"handlers": ["console", "file"],
},
}
logging.config.dictConfig(config)
Use it in your main script:
# main.py
from logging_config import setup_logging
import logging
setup_logging(log_level="DEBUG")
logger = logging.getLogger(__name__)
logger.info("Application started")
logger.debug("Loading configuration...")
Console shows INFO and above (clean output). The file gets everything including DEBUG (full detail for debugging).
print() vs logging
print() | logging | |
|---|---|---|
| Severity levels | No | Yes |
| Timestamps | No | Yes |
| File output | Manual redirect | Built-in |
| Turn on/off | Edit code | Change level |
| Source location | No | Yes |
| Production use | ❌ | ✅ |
Replace print() with logging in any script that will run in production, run on a schedule, or be used by others.
Wrap-Up
The logging module gives you structured, configurable output that scales from a simple debug statement to a full production logging pipeline. Start with basicConfig for simple scripts, and switch to named loggers and handler configuration as your project grows.
For handling the errors that logging records, see the exception handling guide. For running scripts automatically and capturing their log output, see the scheduling guide. For questions or future tutorial ideas, get in touch via the Contact page.