Python Logging for Beginners

· 5 min read · Updated March 13, 2026 · beginner
python stdlib logging debugging

If you have ever used print() statements to debug your Python code, you are not alone. But there is a better way. The logging module gives you control over what messages get displayed, where they go, and how they look. It scales from simple scripts to production applications.

Why Not Just Use Print()

The print() function works for quick debugging, but it has limitations. You cannot easily turn it off in production, filter by importance, or send logs to a file. When your application grows, you need something more flexible.

Logging solves these problems. You can set different levels of importance, redirect output to files or external services, and configure formats consistently. It is built into Python standard library, so no extra installation required.

Your First Logger

Getting started with logging takes only a few lines:

import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

logger.debug("This is a debug message")
logger.info("This is an info message")
logger.warning("This is a warning message")
logger.error("This is an error message")
logger.critical("This is a critical message")

Run this script and you will see messages of different severity levels printed to the console. The default level is WARNING, so debug and info messages are hidden until you change the level.

Understanding Log Levels

Python logging has five standard levels, from least to most severe:

  • DEBUG — Detailed diagnostic information
  • INFO — Confirmation that things work as expected
  • WARNING — Something unexpected, but the program can still run
  • ERROR — A serious problem that prevented part of the program from working
  • CRITICAL — A very serious error that may prevent the program from continuing

Each level has a numeric value. DEBUG is 10, INFO is 20, WARNING is 30, ERROR is 40, and CRITICAL is 50. When you set a level, you get that level and everything above it.

Configuring Output Format

The default output is functional but not pretty. You can customize it with format:

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")

The format string uses placeholder attributes. Common ones include:

  • %(name)s — Logger name
  • %(levelname)s — Log level (DEBUG, INFO, etc.)
  • %(message)s — Your log message
  • %(asctime)s — Timestamp
  • %(filename)s — Source filename
  • %(lineno)d — Line number

This makes it easier to track down where logs came from.

Logging to a File

Printing to the console is fine for development, but production applications need persistent logs. You can write logs to a file:

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
    filename="app.log",
    filemode="a"  # Append mode; use "w" to overwrite
)

logger = logging.getLogger(__name__)
logger.info("This message goes to app.log")

The filemode="a" parameter appends to the file. Change it to "w" if you want each run to start fresh.

You can also log to both console and file using multiple handlers:

import logging

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

# Console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)

# File handler
file_handler = logging.FileHandler("debug.log")
file_handler.setLevel(logging.DEBUG)

# Create formatter and add to handlers
formatter = logging.Formatter("%(levelname)s - %(message)s")
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)

# Add handlers to logger
logger.addHandler(console_handler)
logger.addHandler(file_handler)

logger.debug("Debug message - goes to file only")
logger.info("Info message - goes to both")

This setup sends important messages to the console while keeping detailed logs in a file.

Using Loggers in Your Modules

When you have multiple modules, each should have its own logger:

# mymodule.py
import logging

logger = logging.getLogger(__name__)

def do_something():
    logger.info("Doing something")
    # ... rest of function
# main.py
import logging
import mymodule

logging.basicConfig(
    level=logging.INFO,
    format="%(name)s - %(levelname)s - %(message)s"
)

logger = logging.getLogger(__name__)
logger.info("Starting application")
mymodule.do_something()

Using __name__ as the logger name helps you identify which module produced each log message. This is especially useful in larger applications.

Configuring with Dictionaries

For more complex setups, you can configure logging using a dictionary:

import logging
import logging.config

config = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "detailed": {
            "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
        }
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "level": "INFO",
            "formatter": "detailed",
            "stream": "ext://sys.stdout"
        },
        "file": {
            "class": "logging.FileHandler",
            "level": "DEBUG",
            "formatter": "detailed",
            "filename": "app.log"
        }
    },
    "root": {
        "level": "DEBUG",
        "handlers": ["console", "file"]
    }
}

logging.config.dictConfig(config)
logger = logging.getLogger(__name__)
logger.info("Logging configured with dictionary")

This approach works well with configuration files and makes it easy to change logging behavior without modifying code.

Best Practices

Here are some tips for using logging effectively:

Use the right level. Reserve DEBUG for detailed diagnostics during development. Use INFO for normal operations. WARNING signals something unexpected. ERROR and CRITICAL indicate problems that need attention.

Avoid logging sensitive information. Passwords, API keys, and personal data should never appear in logs.

Log exceptions with traceback information:

try:
    result = 10 / 0
except Exception as e:
    logger.exception("An error occurred")  # Logs the exception with traceback

Use descriptive messages. Instead of “error”, write “Failed to connect to database after 3 retries”.

Keep logger names consistent. Using __name__ in each module is the standard approach.

Getting Started

The logging module is powerful enough for simple scripts and configurable enough for complex applications. Start with basicConfig for quick needs, then move to handlers and formatters as your requirements grow.

Replace your print() debugging statements with proper logging calls. Your future self will thank you when debugging production issues.

See Also

  • logging-module — Full reference for the logging module
  • sys-module — System-specific parameters and functions
  • os-module — Operating system interface for file operations