Logging Events with Python's logging Module
Record useful runtime events with named loggers, severity levels, handlers, formatters, and exception details.
Intermediate
Use print() for ordinary output meant for a person using a program. Use logging when a program should record events with severity and context so developers or operators can understand what happened later. Raise an exception when an operation cannot continue normally; use warnings.warn() for certain avoidable API-use problems that a caller should fix. Logging complements these tools rather than replacing them.
Start with a named logger and useful levels
A logger creates records about events. The common levels, from least to most severe, are DEBUG, INFO, WARNING, ERROR, and CRITICAL. The default root logger threshold is WARNING, so DEBUG and INFO records are normally hidden until logging is configured. In an application entry point, call basicConfig() before the first logging call; in a module, prefer a named logger from getLogger(__name__) so the application can decide where messages go.
Output
Ready to run.
Quick check
Which tool is the better fit for recording a normal runtime event that may be useful during troubleshooting?
import logging
logger = logging.getLogger("demo")
logger.info("Import complete")
Choose a threshold that matches the task
Set a logger's level to decide which severities it processes. INFO includes INFO, WARNING, ERROR, and CRITICAL while excluding DEBUG. Logger levels and handler levels work together: a record must pass the logger's threshold and then the handler's threshold to be emitted. If a non-root logger has no explicit level, it inherits an effective level through its ancestors.
Output
Ready to run.
Quick check
When a logger is set to INFO, which of these records normally passes its level threshold?
import logging
print(logging.INFO > logging.DEBUG)
Pass variable data as logging arguments
Pass a message template and its values as separate arguments, for example logger.info("Loaded %d rows from %s", row_count, source). The logging package combines these values when the record needs to be formatted. This avoids building an f-string before the logger decides whether the record will be handled. The percent-style placeholders in a logging call are separate from the format string used to lay out the final log record.
Output
Ready to run.
Quick check
Which call lets logging defer interpolation until the message is processed?
import logging
logger = logging.getLogger("demo")
count = 7
logger.info("Processed %d items", count)
Understand loggers, handlers, and formatters
A logger is the interface your code calls. A handler sends records to a destination, such as a stream or file. A formatter controls how each record is displayed. For simple scripts, basicConfig() can set the root logger's level, destination, and format. In larger applications, logger names commonly follow the module path with getLogger(__name__); libraries should usually leave application-wide logging configuration to the application.
Output
Ready to run.
Quick check
Which logging component decides the layout of a record such as "INFO:worker:Started"?
import io
import logging
stream = io.StringIO()
handler = logging.StreamHandler(stream)
handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))
record = logging.LogRecord("worker", logging.INFO, "example.py", 1, "Started", (), None)
handler.handle(record)
print(stream.getvalue(), end="")
handler.close()
Include exception information when handling a failure
Inside an except block, logger.exception() records a message at ERROR level and includes the active exception traceback. Use it when the application is handling the failure and needs to preserve diagnostic context. Logging does not replace raising an exception when the caller must be told that the operation failed.
Output
Ready to run.
Quick check
Where should logger.exception() normally be called to include the active traceback?
import logging
try:
int("x")
except ValueError:
logging.getLogger(__name__).exception("Could not parse input")
Send log records to a file safely
For file output, attach a FileHandler or use the filename option of basicConfig() in a simple application. When creating a handler directly, remove it from the logger and close it when it is no longer needed. The example uses a temporary directory so running it does not leave a log file in the project. Configure logging once at the application boundary instead of repeatedly changing global logging configuration inside reusable functions.
Output
Ready to run.
Key takeaways
- Use print() for ordinary user-facing output, logging for runtime records, warnings.warn() for appropriate caller-fixable API warnings, and exceptions when failure must be communicated to a caller.
- Choose DEBUG, INFO, WARNING, ERROR, or CRITICAL based on the event severity; levels act as thresholds.
- Use getLogger(__name__) in modules and configure destinations at the application boundary.
- Log variable values as separate arguments so interpolation can be deferred, and use logger.exception() inside an except block when a traceback is needed.
- Loggers create records, handlers route them, and formatters control their layout; close handlers your code creates.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.