Python Logging: A Complete Beginner’s Guide

Published on: July 10, 2026
Reading time: 10 minutes
Configuração de logs em aplicações Python usando logging

When a Python program is small, a few print() calls may be enough to understand what it is doing. You can display a variable, check whether a function ran, and remove the temporary messages after fixing the problem. That approach becomes difficult to manage as soon as the project grows, runs in the background, or is deployed to another computer.

Python logging gives you a structured way to record events while an application is running. Instead of producing disposable terminal output, you can classify messages by severity, add timestamps automatically, save records to files, capture complete exception tracebacks, and send different messages to different destinations.

This beginner-friendly guide explains the standard logging module from the ground up. You will learn the five log levels, configure your first logger, write logs to a file, handle exceptions, use handlers and formatters, rotate large log files, and avoid common mistakes. The module is included with Python, so there is nothing extra to install.

What Is Logging in Python?

Logging is the practice of recording events that happen during a program’s execution. A log entry can describe a successful operation, an important state change, a warning, or an error that prevented part of the application from working.

For example, an automation script might record when it starts, how many files it processed, which files failed, and when it finished. A web application might record incoming requests, failed login attempts, database errors, and background tasks. These records make it much easier to understand what happened after the program has already stopped.

If you are still learning the foundations, this introduction to programming for beginners provides useful context. Logging becomes especially valuable once your programs contain several functions, modules, or long-running tasks.

Python Logging vs. print()

The Python print function is excellent for displaying information to a user and for quick experiments. It is not designed to be a complete monitoring system.

The logging module offers several features that print() does not provide by itself:

  • Severity levels such as DEBUG, INFO, WARNING, ERROR, and CRITICAL.
  • Automatic timestamps, module names, function names, and line numbers.
  • Output to the terminal, files, network services, or several destinations at once.
  • Filtering so that development details can be hidden in production.
  • Full exception tracebacks.
  • File rotation to prevent log files from growing forever.
  • Central configuration for an entire application.

You do not need to stop using print() completely. Use it for information intentionally shown to the person running the program. Use logging for information that helps developers and operators understand the application’s behavior.

The Five Standard Logging Levels

Every log message has a level that represents its importance. Python defines five commonly used levels:

LevelWhen to use it
DEBUGDetailed diagnostic information useful during development.
INFOConfirmation that an expected operation happened successfully.
WARNINGAn unexpected situation or potential problem that did not stop the program.
ERRORA failure prevented a particular operation from completing.
CRITICALA severe failure may prevent the application from continuing.

By default, the root logger displays messages at WARNING level or higher. That means a DEBUG or INFO call may produce no visible output until you configure a lower threshold.

Your First Python Logging Example

Import the module and call logging.basicConfig() before sending messages:

import logging

logging.basicConfig(level=logging.DEBUG)

logging.debug("Detailed information for debugging")
logging.info("The application started successfully")
logging.warning("Disk space is running low")
logging.error("The file could not be processed")
logging.critical("The application cannot continue")

Because the configured level is DEBUG, all five messages can be displayed. If you change it to logging.ERROR, only ERROR and CRITICAL messages will pass through the filter.

The level is a threshold, not a command that changes the type of every message. Each call still creates its own level; the configuration determines which levels are allowed to continue.

Formatting Log Messages

A plain message is useful, but real logs usually need context. You can add the time, severity, logger name, module, or other details with the format argument:

import logging

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

logging.info("Application ready")

Common fields include:

  • %(asctime)s: date and time of the event.
  • %(levelname)s: the level name.
  • %(name)s: the logger name.
  • %(module)s: the Python module that created the message.
  • %(lineno)d: the source-code line number.
  • %(message)s: the message you supplied.

The official Python Logging HOWTO provides a practical introduction to these settings. The complete logging module reference documents every logger, handler, formatter, filter, and configuration option.

Adding Variables to Log Messages

You can include dynamic values in a message. Although Python f-strings are convenient, the logging API also supports deferred formatting:

import logging

user_id = 417
filename = "report.csv"

logging.info("User %s started processing %s", user_id, filename)

With this style, the logger receives the template and values separately. Formatting is postponed until the message actually needs to be emitted. That can avoid unnecessary work when a low-priority message is filtered out.

Keep messages specific enough to be useful. “Something failed” gives you very little information. “Could not process report.csv for user 417” makes the next debugging step much clearer.

Saving Python Logs to a File

One of the main reasons to use logging is to keep a history after the terminal closes. Add a filename to the configuration:

import logging

logging.basicConfig(
    filename="application.log",
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(message)s",
    encoding="utf-8",
)

logging.info("Customer import started")
logging.error("Could not save record 208")

Python creates application.log in the program’s current working directory. A relative filename may not always point to the folder containing your script, so larger projects should build an explicit path. The guide to managing files with pathlib shows a clean, cross-platform way to work with paths.

The default file mode is append, so new messages are added to the existing file. Use filemode="w" only when you intentionally want to replace the previous log every time the program starts.

Capturing Exceptions and Tracebacks

A normal error message may tell you that an operation failed, but a traceback shows the sequence of calls that led to the failure. Inside an exception handler, call logger.exception():

import logging

logging.basicConfig(
    level=logging.ERROR,
    format="%(asctime)s | %(levelname)s | %(message)s",
)

try:
    result = 10 / 0
except ZeroDivisionError:
    logging.exception("A mathematical error occurred")

logging.exception() logs at ERROR level and automatically includes the current exception information. It should normally be called inside an except block. You can produce similar output with logging.error(..., exc_info=True).

Logging does not replace good exception handling. First decide whether the error can be recovered from, should be returned to the caller, or should stop the program. This guide to try and except in Python explains the underlying control flow.

Use a Logger for Each Module

Small examples often call functions on the root logging object. In a multi-file application, create a named logger in each module:

import logging

logger = logging.getLogger(__name__)


def load_configuration():
    logger.info("Loading application configuration")

The special value __name__ identifies the module. A message from shop.payments can therefore be distinguished from one created by shop.inventory. Logger names also form a hierarchy, which lets you configure a whole package or one particular module.

A common application pattern is:

  • Library modules create loggers with logging.getLogger(__name__).
  • The application entry point configures handlers and levels.
  • Individual modules emit messages without repeatedly calling basicConfig().

Understanding Handlers and Formatters

A logger creates a record. A handler sends that record to a destination. A formatter controls how the final message looks.

The most common handlers are:

  • StreamHandler for the terminal.
  • FileHandler for a regular file.
  • RotatingFileHandler for files limited by size.
  • TimedRotatingFileHandler for files rotated on a time schedule.

The next example sends INFO and higher messages to the console while saving DEBUG and higher messages to a file:

import logging

logger = logging.getLogger("inventory")
logger.setLevel(logging.DEBUG)

console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)

file_handler = logging.FileHandler(
    "inventory.log",
    encoding="utf-8",
)
file_handler.setLevel(logging.DEBUG)

formatter = logging.Formatter(
    "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
)

console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)

logger.addHandler(console_handler)
logger.addHandler(file_handler)

logger.debug("Detailed inventory calculation")
logger.info("Inventory update completed")

The DEBUG message goes only to the file. The INFO message goes to both destinations. This separation is useful when developers need detailed records without filling the terminal with every internal step.

Prevent Duplicate Log Messages

If the same message appears twice, the code may be adding handlers more than once or allowing a record to propagate to a parent logger that has another handler.

Do not add a new handler every time a function runs. Configure logging once during application startup. When creating an isolated logger, you can also disable propagation:

logger.propagate = False

Use that option deliberately. Propagation is helpful in most package-based applications because it allows a central configuration to handle records from child modules.

Rotate Log Files Before They Grow Too Large

A long-running application can generate a large file. RotatingFileHandler starts a new file after the current one reaches a chosen size:

import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger("service")
logger.setLevel(logging.INFO)

handler = RotatingFileHandler(
    "service.log",
    maxBytes=1_000_000,
    backupCount=3,
    encoding="utf-8",
)

handler.setFormatter(
    logging.Formatter(
        "%(asctime)s | %(levelname)s | %(message)s"
    )
)

logger.addHandler(handler)
logger.info("Service started")

In this example, the active file is limited to approximately one megabyte and up to three backup files are retained. Rotation keeps local storage under control, but you should still choose retention rules that match the application and its operational requirements.

Complete Practical Example

The following script simulates temperature readings. It sends messages to both the terminal and a rotating file, uses different severity levels, and records unexpected exceptions.

import logging
import random
import time
from logging.handlers import RotatingFileHandler

logger = logging.getLogger("temperature_monitor")
logger.setLevel(logging.DEBUG)

formatter = logging.Formatter(
    "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
)

console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)

file_handler = RotatingFileHandler(
    "temperature.log",
    maxBytes=500_000,
    backupCount=2,
    encoding="utf-8",
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)

logger.addHandler(console_handler)
logger.addHandler(file_handler)


def read_temperature():
    return random.uniform(30.0, 105.0)


def run_monitor(iterations=5):
    logger.info("Temperature monitoring started")

    try:
        for _ in range(iterations):
            temperature = read_temperature()
            logger.debug(
                "Raw sensor reading: %.2f C",
                temperature,
            )

            if temperature < 60:
                logger.info(
                    "Temperature is normal: %.2f C",
                    temperature,
                )
            elif temperature < 85:
                logger.warning(
                    "Temperature is elevated: %.2f C",
                    temperature,
                )
            else:
                logger.error(
                    "Temperature exceeded the safe limit: %.2f C",
                    temperature,
                )

            time.sleep(1)

    except KeyboardInterrupt:
        logger.info("Monitoring stopped by the user")
    except Exception:
        logger.exception("Unexpected monitoring failure")
    finally:
        logger.info("Temperature monitoring finished")


if __name__ == "__main__":
    run_monitor()

The console shows the messages most useful during normal operation. The file also receives DEBUG records, which can help when investigating a sensor or calculation problem later.

Python Logging Best Practices

  • Configure once: place the main configuration in the application entry point.
  • Use meaningful levels: do not classify every event as ERROR or INFO.
  • Add context: include useful identifiers such as a filename, task ID, or record ID.
  • Never log secrets: avoid passwords, authentication tokens, private keys, and sensitive personal data.
  • Prefer module loggers: use logging.getLogger(__name__) in reusable modules.
  • Capture tracebacks: use logger.exception() for unexpected failures inside exception handlers.
  • Rotate files: prevent long-running applications from creating unlimited local logs.
  • Keep messages readable: write concise descriptions that explain what happened.
  • Use DEBUG selectively: detailed development information can be noisy and may expose internal data.

Logging is especially useful in unattended Python automation scripts, because the log becomes the record of what happened while no one was watching the terminal. It also works alongside breakpoints and the techniques in this guide to debugging Python in VS Code.

Common Beginner Mistakes

Calling basicConfig() After Logging Has Already Started

basicConfig() may appear to do nothing if the root logger already has handlers. Configure logging at the beginning of the program, before the first log call.

Writing Every Message at the Same Level

Use INFO for normal milestones, WARNING for recoverable concerns, ERROR for failed operations, and DEBUG for detailed diagnostics. Consistent levels make filtering useful.

Logging an Exception Without a Traceback

logger.error("Operation failed") records only that sentence. Inside an exception handler, use logger.exception("Operation failed") when the traceback will help diagnose the problem.

Creating Handlers Repeatedly

If setup code runs several times, every handler may emit the same record. Keep configuration centralized and avoid attaching duplicate handlers.

Recording Sensitive Information

Logs are often copied, archived, searched, or sent to external systems. Treat them as data that other people may access. Mask sensitive values and record only what is necessary.

Frequently Asked Questions

Does logging slow down a Python program?

Logging has a cost, especially when an application creates a very large number of messages or writes to slow destinations. Appropriate levels, deferred message formatting, rotation, and queue-based handlers can reduce the impact. For most ordinary scripts, sensible logging provides much more value than overhead.

Where is a relative log file saved?

It is normally created in the process’s current working directory, which may differ from the script’s directory. Use an explicit path when the location matters.

What is the difference between logger.error() and logger.exception()?

logger.error() records an ERROR message. logger.exception() also records at ERROR level but automatically adds the current exception traceback, so it is intended for use inside an exception handler.

Can Python send logs to more than one destination?

Yes. Attach multiple handlers to the same logger. Each handler can have its own level, formatter, and destination.

Should a reusable library call basicConfig()?

Usually no. A library should create named loggers and allow the application using the library to decide how records are displayed or stored.

Conclusion

Python logging turns scattered debugging messages into a reliable record of application behavior. Start with basicConfig(), learn the five levels, add useful context, and save important records to a file. As your project grows, move to named loggers, handlers, formatters, exception tracebacks, and file rotation.

The goal is not to log every line of code. Good logging records the events that help someone understand what the application did, why an operation failed, and what should be investigated next. Applying these practices early will make your Python projects easier to debug, maintain, and operate.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Dicas para otimizar scripts Python lentos
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    Why Is Your Python Script Slow? Fixes

    Learn why Python scripts run slowly and how to fix it: use built-ins, list comprehensions, sets, generators, NumPy, cProfile profiling,

    Ler mais

    Tempo de leitura: 3 minutos
    03/06/2026
    Problemas de travamento com threading em scripts Python
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    Python Threading: Stop Your Script From Freezing

    Learn Python threading: why scripts freeze, create and start threads, use join(), avoid race conditions with Lock, and use ThreadPoolExecutor

    Ler mais

    Tempo de leitura: 4 minutos
    03/06/2026
    Criação de cliente TCP simples usando Python
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    Build a Simple Python TCP Client

    Build a simple Python TCP client using the socket module: connect to a server, send and receive bytes, handle errors,

    Ler mais

    Tempo de leitura: 3 minutos
    03/06/2026
    Criação de hashes seguros para senhas usando Python
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    Python Password Hashing with bcrypt

    Learn Python password hashing with bcrypt: generate secure hashes, verify passwords, understand salt, and build a complete authentication system.

    Ler mais

    Tempo de leitura: 4 minutos
    03/06/2026
    Uso de dataclasses para simplificar classes em Python
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    Python Dataclasses: Clean Data Classes Easily

    Learn how Python dataclasses work: auto-generate __init__, __repr__, __eq__, use default values, field(), frozen=True, __post_init__, and asdict/astuple.

    Ler mais

    Tempo de leitura: 4 minutos
    03/06/2026
    Entendendo como o GIL afeta performance em Python
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    Python GIL: What It Is and How It Affects Code

    Learn what Python's GIL is, why it exists, how it limits multithreading, and when to use multiprocessing to achieve true

    Ler mais

    Tempo de leitura: 6 minutos
    03/06/2026