Home› Tutorials› Standard Library
Standard LibraryPython Logging: Replacing print() Debugging With logging
- logging separates a message's severity (level) from whether it is shown, so output can be filtered without touching the code.
- The five standard levels, from least to most severe, are DEBUG, INFO, WARNING, ERROR, and CRITICAL.
- logging.getLogger(__name__) gives each module its own logger, so messages show exactly where they came from.
- A logger can send output to the console, a file, or both, at the same time, without changing any logging calls.
Why not just use print()
print() is fine for a quick check, but it does not scale: every statement fires unconditionally, there is no record of severity, and removing debug output later means hunting down and deleting individual print lines across the codebase. The logging module solves all three problems — each message carries a severity level, and a single configuration setting controls which levels actually get shown, application-wide, without touching any of the call sites:
import logging
logging.basicConfig(level=logging.INFO)
logging.info("Server started")
logging.debug("This will not show at INFO level")
Log levels
The standard levels, from least to most severe, are DEBUG, INFO, WARNING, ERROR, and CRITICAL. Setting a level means "show this level and everything more severe":
import logging
logging.basicConfig(level=logging.WARNING)
logging.debug("Detailed diagnostic info") # hidden
logging.info("Routine status update") # hidden
logging.warning("Something looks off") # shown
logging.error("An operation failed") # shown
logging.critical("Unrecoverable failure") # shown
Choosing DEBUG during development and WARNING or INFO in production is the usual pattern — verbose detail while building the feature, quieter output once it is stable.
Configuring logging with basicConfig
logging.basicConfig() sets up the root logger in one call, and accepts a format string to control what each line looks like:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logging.info("Job completed")
# 2026-07-06 10:15:03,221 [INFO] root: Job completed
basicConfig() only has an effect the first time it is called in a process — later calls are silently ignored unless you pass force=True — so it is normally called once, near the entry point of a program.
Named loggers per module
Rather than calling the top-level logging.info() functions everywhere, each module should create its own logger with logging.getLogger(__name__). __name__ resolves to the module's dotted path, so log output shows exactly where each message originated:
# in payments.py
import logging
logger = logging.getLogger(__name__)
def charge(amount):
logger.info("Charging %s", amount)
Named loggers form a hierarchy that mirrors your package structure, which means you can raise or lower the verbosity for one specific module — for example, silencing noisy debug output from a third-party library — without touching the levels of everything else.
Note the %s placeholder style rather than an f-string inside the logging call itself: logger.info("Charging %s", amount) only formats the string if the message will actually be emitted at the current level, avoiding wasted work for suppressed debug-level calls.
Logging to a file
A FileHandler sends log records to a file instead of, or in addition to, the console. Multiple handlers can be attached to the same logger, each with its own level and format:
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
file_handler = logging.FileHandler("app.log")
file_handler.setLevel(logging.WARNING)
file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
logger.addHandler(file_handler)
logger.warning("Disk space running low")
This setup writes only WARNING and above to app.log, while the logger itself is still open to processing DEBUG-level calls that other handlers, such as a console handler with a lower threshold, might display.
Logging exceptions with logger.exception()
Inside an except block, logger.exception(message) logs the message at ERROR level and automatically attaches the full traceback of the exception currently being handled — no need to format it yourself:
import logging
logger = logging.getLogger(__name__)
try:
result = 10 / 0
except ZeroDivisionError:
logger.exception("Failed to compute result")
The logged output includes the message plus the complete traceback, exactly as it would appear if the exception had been left uncaught, which is invaluable when a failure happens in production and there is no interactive debugger available. logger.exception() is only meaningful inside an except block, since it relies on Python's currently-handled-exception context; calling it elsewhere logs the message with no traceback attached.
Quieting noisy third-party loggers
Because loggers form a hierarchy keyed by dotted module name, you can raise the threshold for one specific library without changing your own application's verbosity at all:
import logging
logging.basicConfig(level=logging.DEBUG) # your own code stays verbose
logging.getLogger("urllib3").setLevel(logging.WARNING) # this library gets quieter
Every message a library logs through logging.getLogger("urllib3.something") still passes through the "urllib3" logger's effective level on its way up the hierarchy, so setting the parent's level to WARNING silences its routine DEBUG and INFO chatter while leaving your own application's loggers, configured separately, exactly as verbose as you set them. This is usually far preferable to lowering the root logger's own level, which would also quiet legitimate warnings coming from your own code.