Python Logging

Learn Python logging with simple examples. Configure logging using basicConfig(), log to console and file, use log levels, create loggers, handlers, formatters, and avoid common mistakes.

Published

Updated

Read time 8 min read

Reviewed byDeepak Prasad

Python Logging

Python’s standard logging module records events while your program runs. Each event has a level (for example DEBUG or ERROR) so you can filter noise in development and keep useful signals in production. This guide follows a practical order: a tiny example, basicConfig(), levels, files, then named loggers, handlers, formatters, configuration for larger apps, and common mistakes—with runnable snippets where they help.

Tested on: Python 3.13.3; kernel 6.14.0-37-generic; Ubuntu 25.04.


What is logging in Python?

The logging HOWTO describes logging as a way to track events that happen while software runs, where each event has an importance level. You emit a log record; the library decides whether to pass it to handlers (console, file, syslog, and so on) and how to format it. That separation—log call vs destination vs layout—is what makes logging scale from scripts to multi-package applications.


Basic Python logging example

Import logging

Every script starts the same way:

python
import logging
Output

Log messages with warning(), info(), error(), and debug()

The module-level functions logging.debug, logging.info, logging.warning, logging.error, and logging.critical send messages to the root logger. By default the root logger’s level is WARNING, so info and debug are dropped until you lower the level (next section).

python
import logging

logging.warning("Disk usage above 80%")
logging.error("Payment gateway timeout")
Output

When you run this, you should see two lines on stderr, each starting with the level name and the message. With the default configuration, logging.info("started") would print nothing until you configure the level.


Configure logging with basicConfig()

logging.basicConfig(**kwargs) configures the root logger: level, format, and optionally a default file handler. It is intended to run once near the start of main; if the root handlers are already configured, later calls do nothing— a common source of confusion.

Set logging level

python
import logging

logging.basicConfig(level=logging.INFO)
logging.debug("not shown")
logging.info("shown")
Output

You should see one INFO line; the DEBUG line is filtered out because the threshold is INFO.

Set log format

Use a format string-style template with logging’s own fields (see the formatter section). message is the text you pass to info(), error(), and so on.

python
import logging

logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logging.info("Server listening on :8080")
Output

Add timestamp to logs

%(asctime)s adds a human-readable time; datefmt controls its shape.

python
import logging

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

Log to a file

filename= attaches a FileHandler to the root logger. Paths are relative to the process working directory. Snippets that create files on disk are not suitable for the in-browser Run control, so the next example is marked non-runnable—run it locally. Confirm the parent directory exists (or create it) before pointing a handler at a nested path—see check if a file exists and create nested directories.

python
import logging

logging.basicConfig(
    filename="app.log",
    level=logging.DEBUG,
    format="%(asctime)s %(levelname)s %(message)s",
)
logging.info("written to app.log in the current directory")

Use filemode to overwrite or append

filemode="w" truncates the file each run; the default is "a" (append). Pair append mode with rotation (see RotatingFileHandler below) if the file can grow without bound.


Python logging levels

Numeric order matters: DEBUG is lowest, CRITICAL is highest. A logger or handler set to INFO accepts INFO, WARNING, ERROR, and CRITICAL.

Level When to use it
DEBUG Verbose diagnostics, often disabled in production.
INFO Normal lifecycle events (startup, job completed).
WARNING Something unexpected but the program continues.
ERROR A serious problem in a specific operation.
CRITICAL Failure that may abort the whole program.

Use DEBUG for stepping through logic and verbose traces (usually off in production). INFO is the default “business as usual” channel: startup, batches completed, configuration loaded. WARNING covers deprecations, retries, and resource pressure—something to review without treating it as a hard failure. ERROR is for a failed operation (I/O, dependency 5xx, bad input for one request). CRITICAL is for conditions that may require stopping the process—security failure, corruption risk, or cannot bind a required port.


Python logger example

Create logger with getLogger(name)

logging.getLogger(__name__) gives one logger per module. The logger name appears in formatted output and helps you trace which package emitted a line.

python
import logging

logging.basicConfig(level=logging.INFO, format="%(name)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)
logger.info("module loaded")
Output

Use logger instead of root logging functions

Calling logger.info(...) keeps names consistent and plays well with hierarchy: child loggers can inherit or override levels and handlers. For libraries, prefer getLogger(__name__) and avoid calling basicConfig() yourself—let the application configure logging.

Logger level vs handler level

Both ends filter records. The effective level is the more restrictive combination: a record must pass the logger’s level and every handler it reaches. A logger at DEBUG with a handler at INFO still only prints INFO and above on that handler. If you see “missing” logs, check both the logger setLevel and each handler’s level.


Python logging handler example

Handlers decide where records go. After basicConfig(), you usually get a single StreamHandler on stderr. For custom setups you build a logger, attach one or more handlers, and assign formatters per handler.

StreamHandler for console logs

python
import logging
import sys

logger = logging.getLogger("demo")
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.INFO)
handler.setFormatter(logging.Formatter("%(levelname)s %(message)s"))
logger.addHandler(handler)
logger.info("to stdout")
Output

FileHandler for log files

FileHandler appends or overwrites a path on disk. To learn more about file I/O in general, see write to a file in Python.

python
import logging

logger = logging.getLogger("filedemo")
logger.setLevel(logging.DEBUG)
fh = logging.FileHandler("demo.log", mode="a", encoding="utf-8")
fh.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
logger.addHandler(fh)
logger.info("persisted line")

RotatingFileHandler for rotating logs

When logs grow large, rotate by size (or use TimedRotatingFileHandler for time-based rotation). Size-based rotation ties naturally to monitoring file size on disk.

python
import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger("rotate")
logger.setLevel(logging.INFO)
rh = RotatingFileHandler("app.log", maxBytes=1_000_000, backupCount=3, encoding="utf-8")
rh.setFormatter(logging.Formatter("%(message)s"))
logger.addHandler(rh)
logger.info("line")

Other handlers (SocketHandler, HTTPHandler, SMTPHandler, SysLogHandler, and more) follow the same pattern: create, optionally set level and formatter, addHandler. See the Logging Cookbook for email, network, and queue patterns.


Python logging formatter example

Format message, level, time, and module name

python
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(name)s | %(levelname)s | %(message)s",
)
logging.getLogger(__name__).info("formatted line")
Output

Useful formatter fields

Common LogRecord attributes for logging.Formatter:

  • %(asctime)s — time of the record (combine with datefmt=).
  • %(name)s — logger name.
  • %(levelname)s — DEBUG, INFO, etc.
  • %(message)s — the logged text.
  • %(filename)s, %(lineno)d — source location (great for debugging).
  • %(funcName)s — enclosing function name.
  • %(threadName)s / %(process)d — concurrency and process identity.

Log exceptions in Python

Use logging.exception()

Inside an except block, logger.exception("context") logs at ERROR and appends the traceback automatically.

python
import logging

logging.basicConfig(level=logging.DEBUG)

try:
    1 / 0
except ZeroDivisionError:
    logging.exception("division failed")
Output

You should see an ERROR line plus a traceback block.

Use exc_info=True

When you are not inside except but still have an exception object—or when you use error instead of exception—pass exc_info=True (or exc_info=sys.exc_info() in an except handler before the stack unwinds).

python
import logging

logging.basicConfig(level=logging.ERROR)

try:
    {}["missing"]
except KeyError as e:
    logging.error("lookup failed: %s", e, exc_info=True)
Output

For broader error handling patterns, see try / except in Python. To log truly uncaught exceptions at process exit, set sys.excepthook to a function that calls logger.error(..., exc_info=...); keep that in application code rather than library imports.


Configure logging for larger applications

When basicConfig() is enough

Scripts, notebooks, and small CLIs: one basicConfig(level=..., format=...) at entry, then module loggers with getLogger(__name__), is enough. Avoid calling basicConfig() from library packages. CLI tools often wire verbosity from Python argparse flags into logging.basicConfig(level=...).

When to use dictConfig()

logging.config.dictConfig accepts a Python dictionary (often loaded from JSON or YAML) describing loggers, handlers, formatters, and filters. It is easier to generate and diff than INI, and fits dynamic deployment settings.

python
import logging
from logging.config import dictConfig

dictConfig({
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "brief": {"format": "%(levelname)s %(name)s: %(message)s"},
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "brief",
            "level": "INFO",
        },
    },
    "root": {"level": "DEBUG", "handlers": ["console"]},
})
logging.getLogger("app").info("from dictConfig")
Output

When to use fileConfig()

logging.config.fileConfig reads classic INI-style files with [loggers], [handlers], and [formatters] sections. It suits teams that already maintain .ini configuration and want logging settings outside Python source. Load it once at startup:

python
import logging.config

logging.config.fileConfig("logging_config.ini")
logging.getLogger(__name__).info("configured from INI")

INI examples belong in a real file next to your app; the exact sections mirror the dictionary keys above (logger names, handler classes, formatter strings).


Common mistakes with Python logging

  • Calling basicConfig() twice and wondering why the second call does nothing—configure once, or switch to dict/file config.
  • Expecting logging.info to print with the default root level still at WARNING—set level=logging.INFO (or lower) in basicConfig or on your handlers.
  • Attaching handlers to a named logger and leaving propagate=True (default), which duplicates records on the root logger—set logger.propagate = False when you manage handlers only on that logger.
  • Using string concatenation for log messages; prefer logger.debug("user %s", user_id) so interpolation runs only if the level is enabled.
  • Forgetting UTF-8 when writing log files on Windows—pass encoding="utf-8" to FileHandler / basicConfig where supported.

Python logging quick reference table

Goal Typical API
Quick script setup logging.basicConfig(level=..., format=...)
Per-module logger logger = logging.getLogger(__name__)
Console output StreamHandler(sys.stderr) (default with basicConfig) or stdout
File output FileHandler("app.log") or filename= in basicConfig
Large files RotatingFileHandler / TimedRotatingFileHandler
Custom layout logging.Formatter("...%(levelname)s...")
Exception context logger.exception("msg") or exc_info=True
Centralized config dictConfig / fileConfig

Summary

Python logging separates what happened (the log call and level), where it goes (handlers), and how it looks (formatters). Start with import logging, a minimal basicConfig() for level and format, then graduate to getLogger(__name__), explicit handlers, and dictConfig or fileConfig when multiple destinations and teams need maintainable configuration. Use exception() or exc_info=True so errors carry stack traces, and watch logger vs handler levels plus basicConfig’s “only once” behavior to avoid silent misconfiguration.


References


Frequently Asked Questions

1. What is the difference between logging.basicConfig() and logging.getLogger()?

basicConfig() configures the root logger once (level, format, default handler); getLogger(name) returns a logger object—often logging.getLogger(name) per module—so you attach handlers and formatters yourself when you outgrow basicConfig().

2. Why does my logger print nothing after I skip basicConfig()?

If the effective level is OK but no handler is attached, the last-resort handler may still show WARNING+ on the root logger; for a named logger with propagate False you must add handlers or rely on propagation to the root.

3. When should I use logging.exception() instead of logging.error()?

Inside an except block, logging.exception(msg) logs at ERROR and automatically attaches exception info; logging.error(..., exc_info=True) does the same when you need a custom level or message pattern.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …