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:
import loggingLog 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).
import logging
logging.warning("Disk usage above 80%")
logging.error("Payment gateway timeout")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
import logging
logging.basicConfig(level=logging.INFO)
logging.debug("not shown")
logging.info("shown")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.
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logging.info("Server listening on :8080")Add timestamp to logs
%(asctime)s adds a human-readable time; datefmt controls its shape.
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")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.
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.
import logging
logging.basicConfig(level=logging.INFO, format="%(name)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)
logger.info("module loaded")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
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")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.
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.
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
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(name)s | %(levelname)s | %(message)s",
)
logging.getLogger(__name__).info("formatted line")Useful formatter fields
Common LogRecord attributes for logging.Formatter:
%(asctime)s— time of the record (combine withdatefmt=).%(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.
import logging
logging.basicConfig(level=logging.DEBUG)
try:
1 / 0
except ZeroDivisionError:
logging.exception("division failed")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).
import logging
logging.basicConfig(level=logging.ERROR)
try:
{}["missing"]
except KeyError as e:
logging.error("lookup failed: %s", e, exc_info=True)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.
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")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:
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.infoto print with the default root level still at WARNING—setlevel=logging.INFO(or lower) inbasicConfigor on your handlers. - Attaching handlers to a named logger and leaving
propagate=True(default), which duplicates records on the root logger—setlogger.propagate = Falsewhen 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"toFileHandler/basicConfigwhere 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.

