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:
| Level | When to use it |
|---|---|
| DEBUG | Detailed diagnostic information useful during development. |
| INFO | Confirmation that an expected operation happened successfully. |
| WARNING | An unexpected situation or potential problem that did not stop the program. |
| ERROR | A failure prevented a particular operation from completing. |
| CRITICAL | A 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:
StreamHandlerfor the terminal.FileHandlerfor a regular file.RotatingFileHandlerfor files limited by size.TimedRotatingFileHandlerfor 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 = FalseUse 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.






