How To Configure Logging In Python

As a Python program runs, its messages are more useful when each one shows its severity and goes to the right place. Python’s logging package lets your code create these records and send them to the console or a file.

I’ll show how to configure logging output and record an exception with its traceback.

TL;DR: Configure Python logging in one minute

Use Python’s standard-library logging package to set a severity threshold, format each record and route it to a console or file handler. For a short script, basicConfig configures the root logger. Add handlers or dictConfig when an application needs separate destinations.

  • DEBUG is the lowest standard level, while CRITICAL is the highest.
  • A logger creates records, handlers route them, and formatters render them.
  • basicConfig affects the root logger and normally does nothing after handlers have been configured.

What is Python logging configuration?

Python’s logging package gives modules a common API for reporting events. The table separates the objects that create, route and render records.

A logger is the named object your code calls, such as checkout. A LogRecord carries the event details, including severity and message. Filters can reject records that do not meet an application-specific condition.

ComponentResponsibilityExample
LoggerCreates records at a named point in the programlogging.getLogger(__name__)
HandlerSends a record to a stream or fileStreamHandler or FileHandler
FormatterChooses the text layoutTimestamp, level and message
FilterAllows or rejects selected recordsKeep records for one subsystem

Each handler can use a different formatter, so the console can show a short severity and message while a file includes timestamps and module names for later searches. This lets each destination serve its reader without changing the logger calls.

Calls such as logging.warning() use the root logger. A named logger can pass a record to a handler attached higher in its hierarchy, leaving the destination to application configuration.

What you need before configuring logging

The logging package ships with Python, so a basic setup needs no third-party installation. Decide where the application starts, what events should be visible in normal operation, and whether records need to persist after the process exits.

  • Put application-wide settings near the program entry point.
  • Choose a threshold that matches the environment, and keep sensitive values out of messages.
  • For a file, choose a path the process can write and decide whether the file should append or be replaced.
  • Reusable modules can emit records without choosing the application’s output policy.

Call basicConfig before the first log event in a simple script. It sets up root logging but leaves existing root handlers in place. If a framework configures logging before your code runs, use that framework’s configuration point or configure explicit handlers.

How to configure Python logging step by step

This sequence starts with the smallest useful root configuration and then adds record fields, destinations and exception context. The examples use standard-library APIs, so they work without installing a logging package.

Step 1: Set a root level with basicConfig

basicConfig sets up the root logger for a simple script. The level parameter sets the minimum severity the root accepts, so DEBUG allows every standard level while WARNING suppresses DEBUG and INFO records.

import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.debug("diagnostic detail")
logger.info("service started")
logger.warning("retrying request")

In a normal standalone run, the DEBUG call is filtered out and INFO and WARNING appear. The default format includes the level, logger name and message. The force=True option replaces existing root handlers, so use it only when the application owns that reset.

A library should avoid changing its host process configuration. Use a named logger in reusable code.

Step 2: Add timestamps and useful fields

A formatter can include time, severity, logger name and message in each line. Set the format on the root with basicConfig for the simple case, and pass arguments separately from the message so logging can defer interpolation until it needs to emit the record.

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger(__name__)
logger.info("loaded %d products", 12)

The percent-style placeholders in the message are supplied as arguments to logger.info(). This keeps the message template and values separate. The formatter’s fields such as asctime and levelname are LogRecord attributes, so a misspelled field can trigger a formatting error when that record is emitted.

Choose thresholds and fields for the runtime, and keep sensitive values out of records.

Step 3: Write logs to a file

Pass filename to basicConfig to send root records to a disk file. The path is interpreted relative to the process’s current working directory when it is relative, not automatically relative to the Python source file.

import logging

logging.basicConfig(
    filename="application.log",
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
    encoding="utf-8",
)
logging.getLogger(__name__).info("report saved")

By default, a file handler appends to an existing file. Set filemode=”w” only when replacing prior contents is intended.

The encoding option makes the file’s text encoding explicit on supported Python versions. Check the logging reference if an older interpreter is in scope.

The parent directory must exist, and the process account needs write permission.

For a relative path such as logs/application.log, create the logs directory before starting the program. Use a rotating handler when the application needs to limit file size.

Step 4: Send logs to both console and file

Attach a StreamHandler and FileHandler when one record needs to go to both console and disk, then set each handler’s level or format for its destination.

import logging

logger = logging.getLogger("inventory")
logger.setLevel(logging.DEBUG)
logger.handlers.clear()
logger.propagate = False

console = logging.StreamHandler()
console.setLevel(logging.WARNING)
console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
file_handler = logging.FileHandler("inventory.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s"))
logger.addHandler(console)
logger.addHandler(file_handler)

logger.info("stock count completed")
logger.warning("reorder threshold reached")

The console handler accepts WARNING and above, while the file handler keeps DEBUG and above.

Clearing handlers makes this standalone example predictable on a repeated run. Do not remove handlers owned by a framework or another part of an application.

StreamHandler writes to stderr by default. Pass sys.stdout when a program specifically requires standard output.

Propagation is disabled here to prevent another emission by root handlers.

Step 5: Use a named logger

Create a module logger with logging.getLogger(__name__) instead of calling the root convenience functions throughout an application. The module name becomes part of the logger hierarchy, which gives an application a way to adjust one package without changing every call site.

import logging

logger = logging.getLogger(__name__)

def load_catalog(path):
    logger.info("loading catalog from %s", path)
    try:
        with open(path, encoding="utf-8") as catalog:
            return catalog.read()
    except OSError:
        logger.exception("catalog could not be read")
        raise

Dotted logger names let an application tune a package such as shop.catalog separately from other modules.

Keep module-level settings focused on emitting useful records. The application chooses the destination and threshold when it starts.

Step 6: Configure handlers with dictConfig

dictConfig describes loggers, handlers and formatters in a dictionary and applies them together. It is useful when configuration has multiple destinations or when the application keeps settings in one structured object.

import logging
import logging.config

config = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "brief": {"format": "%(levelname)s %(name)s: %(message)s"}
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "level": "INFO",
            "formatter": "brief",
        }
    },
    "root": {"level": "DEBUG", "handlers": ["console"]},
}
logging.config.dictConfig(config)
logging.getLogger("inventory").debug("record kept below console threshold")
logging.getLogger("inventory").info("catalog loaded")

The root accepts DEBUG, but the console handler starts at INFO, so that debug call produces no output.

The disable_existing_loggers setting is a policy decision. Leaving it true can disable previously created non-root loggers that are not named in the dictionary. Setting it false preserves them, which is often appropriate when configuring an application that imports libraries before applying dictConfig.

Step 7: Record exceptions with tracebacks

Call logger.exception() inside an except block when the log needs the active exception traceback. It records at ERROR level by default and adds exception information, so the output shows the failure location as well as a short message.

import logging

logging.basicConfig(level=logging.DEBUG, format="%(levelname)s:%(name)s:%(message)s")
logger = logging.getLogger("checkout")
logger.debug("checking stock for SKU-42")
logger.info("order accepted")
try:
    int("not-a-number")
except ValueError:
    logger.exception("could not parse quantity")
DEBUG:checkout:checking stock for SKU-42
INFO:checkout:order accepted
ERROR:checkout:could not parse quantity
Traceback (most recent call last):
  File "logging_basic_demo.py", line 8, in <module>
    int("not-a-number")
ValueError: invalid literal for int() with base 10: 'not-a-number'
Python logging output with DEBUG, INFO and an exception traceback

I ran logging_basic_demo.py and saw its DEBUG, INFO and ERROR records followed by the caught ValueError traceback. Because the except block handles the error, the program completes with exit code 0.

Logging variable data and choosing a message format

Pass changing values as arguments to the logging method rather than assembling a message with string concatenation. The logging call keeps the format string separate from its values until a handler needs to render the record, and the resulting text remains readable in the configured output.

product_id = "BK-204"
remaining = 3
logger.info("product %s has %d items left", product_id, remaining)

The two placeholders correspond to the values after the format string. Use conversions that match the data, such as %s for text and %d for an integer. Avoid putting sensitive values in either the message or its arguments because formatters and handlers can write them to persistent logs.

For structured context, use the extra argument to attach additional LogRecord attributes, then reference those attributes in a custom formatter. The attribute names must not collide with fields already used by logging. A missing field can cause formatting to fail when the record reaches that formatter, so test the configured output path with representative records.

Change the displayed message format

The format string controls what a rendered line contains and in what order. A concise console format helps when watching a running program, while a detailed file format can include the logger name and source location for investigation.

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

Common LogRecord fields include levelname, name, message, filename, lineno and asctime. Add only fields that help answer a question during debugging. A very wide format can make terminal output difficult to scan, while a format that omits the logger name can make records from imported modules harder to identify.

Display the date and time in each record

Include %(asctime)s in the format to display the time when the record was created. The datefmt argument controls its presentation using the directives accepted by Python’s time formatting functions.

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

Choose a timestamp format that is consistent across the application’s destinations. The shown format is readable and sortable by date, but it does not label a timezone. If logs from machines in different zones will be compared, configure a consistent time convention and make that convention clear to the people reading the files.

Next steps for a logging setup

Once records reach the intended destinations, review what happens as the application grows. A single file handler is suitable for a small script, but a long-running service needs a plan for file growth, retention and collection.

  • Use a rotating handler when local files need a size or time-based retention limit.
  • Keep the logging setup at the application boundary and let modules use named loggers.
  • Check the effective logger and handler levels when an expected message is absent.
  • Review logged fields for secrets and personal data before sending records to shared storage.

Python’s logging cookbook documents queue-based logging, custom handlers and other setups for larger programs. Start with the smallest arrangement that meets the runtime needs, then add a handler only when it addresses a destination or retention requirement.

Fix common Python logging problems

For a missing message, compare the effective logger level with the receiving handler levels. For duplicates, inspect propagation and handlers attached to ancestors.

SymptomLikely causeCheck
INFO or DEBUG is missingRoot or named logger threshold is higherCheck logger.getEffectiveLevel()
Logger calls appear to do nothingbasicConfig ran after a handler already existedFind who configured root first
File is missingRelative path points at a different working directory, or directory is absentPrint Path.cwd() and verify directory permissions
Each line appears twiceA child handler emits locally and propagation reaches a parent handlerInspect ancestor handlers and propagation
File contains old recordsFileHandler appends by defaultChoose append or deliberate filemode=”w”
Formatting raises an errorFormatter field is absent or misspelledUse documented LogRecord attribute names

A useful first check is logger.getEffectiveLevel(), which reports the level inherited through the logger hierarchy when the logger has no explicit level. Then inspect handler levels. Check the handler threshold too: an INFO handler filters out DEBUG records.

In a notebook or framework, inspect who added root handlers before calling basicConfig.

Find the application’s startup configuration instead of adding another setup call at an arbitrary import point.

A relative filename follows the process working directory, which can differ between an interactive shell, an IDE and a service manager. Use an explicit path when the destination must be stable, and create the parent directory before opening a FileHandler. Permission errors occur when the process account cannot write to that destination.

Duplicate records often come from propagation. A child with its own handler can produce duplicates when propagation also reaches a parent handler.

Prefer a consistent handler at one suitable level of the hierarchy. Set propagate=False only when the child should stop sending records upward.

Conclusion: Configure logging where the application starts

Configure output at the application entry point, then let modules emit through their own named loggers.

Start with the Python Logging HOWTO, then read AskPython’s Python Logging Module article and guide to listing existing loggers.

FAQ

These answers cover the default level, file destination and the difference between logger and handler thresholds.

What is the default logging level in Python?

The root logging setup defaults to WARNING, so DEBUG and INFO records are not emitted unless you configure a lower threshold.

Where does Python save a log file?

A relative filename is resolved from the process current working directory. Use an explicit path when the file must have a stable location.

How do I log to a file and the console?

Attach a FileHandler and a StreamHandler to the same logger, then set each handler’s level and formatter for its destination.

Why does basicConfig not change my logging output?

basicConfig normally does nothing when the root logger already has handlers. Configure logging at the application’s startup boundary.

Snehal Gokhale
Snehal Gokhale
Articles: 41