Python logging Module: A Complete Guide

· 5 min read

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:

LevelValueWhen to Use
DEBUG10Detailed information for diagnosing problems
INFO20Confirmation that things are working as expected
WARNING30Something unexpected happened, but the program still works
ERROR40A serious problem — the program could not do something
CRITICAL50A 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:

VariableContent
%(asctime)sTimestamp
%(name)sLogger name
%(levelname)sLevel (DEBUG, INFO, etc.)
%(message)sThe log message
%(filename)sSource file name
%(lineno)dLine number
%(funcName)sFunction 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()logging
Severity levelsNoYes
TimestampsNoYes
File outputManual redirectBuilt-in
Turn on/offEdit codeChange level
Source locationNoYes
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.