Get AI summaries of any video or article — Sign up free
Python Tutorial: Logging Advanced - Loggers, Handlers, and Formatters thumbnail

Python Tutorial: Logging Advanced - Loggers, Handlers, and Formatters

Corey Schafer·
5 min read

Based on Corey Schafer's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.

TL;DR

Avoid relying on the root logger when multiple modules call logging.basicConfig; imports can silently reconfigure global logging state.

Briefing

Python’s logging becomes reliable only when each module uses its own logger and explicitly wires that logger to the right outputs. The core fix in this tutorial is moving away from the root logger—where configuration can be overwritten or silently ignored—and toward per-module loggers built with dedicated handlers and formatters. That shift matters because it prevents “missing” log files, mismatched log levels, and inconsistent formatting when multiple files in a project share the same global logging state.

The walkthrough starts by recreating the earlier setup: a simple script writes to sample.log using logging.basicConfig with a DEBUG level and a custom format (time, logger name, message). A second module (employee) also configures logging, but with a different file (employee.log), an INFO level, and a different format. When both modules rely on the root logger, importing one module can change the root logger’s configuration for the entire process. The result is counterintuitive behavior: importing employee creates employee.log but leaves sample.log absent, because the root logger has already been configured to INFO, so DEBUG messages from the simple script never pass the threshold. Even when INFO messages do appear, the shared root logger still produces “messy” outcomes—wrong destinations, wrong levels, and wrong formatting.

To correct this, the tutorial introduces a module-specific logger using logging.getLogger with a name derived from the module’s __name__ (double underscore name). Each module then logs through its own logger variable (e.g., logger.debug or logger.info), allowing the hierarchy to fall back to the root logger only when needed. The next step is to stop using basicConfig for module output and instead attach handlers directly to the module logger. For employee, a FileHandler is created for employee.log, a Formatter is attached to that handler, and the logger’s level is set (e.g., INFO). After removing the basicConfig call, reruns produce employee.log with the expected formatting and the correct logger name.

The same pattern is applied to the simple script: a dedicated logger writes to sample.log with its own formatter and level. Once both modules are separated, the project gains flexibility. Handler-level thresholds can further refine output: setting the sample FileHandler to logging.ERROR filters out DEBUG logs while still allowing the module logger to remain at DEBUG. The tutorial also demonstrates richer error reporting by switching from logging.error to logging.exception inside an except block, which automatically includes the traceback.

Finally, the tutorial shows how modular logging scales by adding multiple handlers to one logger. A StreamHandler can be attached alongside the FileHandler so debug messages appear in the console while errors continue to be written to the log file. With handlers and formatters configured per module, logging becomes predictable, debuggable, and easier to extend—whether that means adding console output, email alerts, or rotating logs later.

Cornell Notes

The tutorial shows why Python logging breaks down when multiple modules share the root logger. Importing a module that calls logging.basicConfig can reconfigure the root logger’s file, level, and format for the entire program, causing missing log files and filtered-out messages (e.g., DEBUG never reaching an INFO root logger). The fix is to create a per-module logger with logging.getLogger(__name__), then attach dedicated handlers (FileHandler and optionally StreamHandler) and attach formatters to those handlers. Handler-level log thresholds enable fine-grained control, and using logging.exception inside an except block records tracebacks automatically. This modular setup makes logging predictable and easier to extend.

Why do sample.log entries disappear after importing the employee module?

Both modules initially rely on the root logger via logging.basicConfig. When employee is imported, its basicConfig runs first and sets the root logger’s level to INFO and its output to employee.log. Later, the simple script’s DEBUG calls go through the same root logger, but DEBUG is below INFO, so those messages never get emitted—explaining why sample.log may not be created or may remain empty.

How does using logging.getLogger(__name__) prevent cross-module interference?

Each module creates its own named logger with logging.getLogger(__name__). Logging calls then use that logger variable (e.g., logger.debug/info) rather than the global logging module defaults. Because each logger can have its own handlers, file destinations and formatting stay tied to the module that configured them, instead of being overwritten by whichever module imported first.

What role do handlers and formatters play compared with basicConfig?

Handlers define where log records go (FileHandler writes to a file; StreamHandler writes to the console). Formatters define how records look (timestamp, logger name, message). In this tutorial, formatters are attached to handlers (e.g., file_handler.setFormatter(formatter)), and the module logger’s level is set separately (e.g., logger.setLevel(logging.INFO). This replaces the coarse global behavior of basicConfig.

How can the tutorial filter only errors into sample.log while keeping the logger at DEBUG?

Keep the module logger level at DEBUG, but set the FileHandler’s level to logging.ERROR. With that configuration, DEBUG and INFO records are generated but not handled by the FileHandler because they don’t meet the handler threshold. When an error occurs, the FileHandler accepts it and writes it to sample.log.

What’s the difference between logging.error and logging.exception in this setup?

logging.error logs an error message without automatically including the traceback. logging.exception, used inside an except block, logs the error message and also appends the traceback for the exception, giving more context for debugging (the tutorial demonstrates this by dividing by zero).

How does adding a StreamHandler change logging behavior?

A StreamHandler is attached to the same module logger alongside the FileHandler. That means debug statements can appear in the console while errors still go to the file. The tutorial also shows assigning a formatter to the StreamHandler so console output matches the desired format.

Review Questions

  1. What specific configuration change caused DEBUG messages to stop appearing after importing another module?
  2. In the per-module approach, which component decides the output destination: the logger or the handler?
  3. How does setting a FileHandler level to logging.ERROR affect messages when the logger level remains at logging.DEBUG?

Key Points

  1. 1

    Avoid relying on the root logger when multiple modules call logging.basicConfig; imports can silently reconfigure global logging state.

  2. 2

    Create a per-module logger with logging.getLogger(__name__) and log through that logger variable (e.g., logger.debug/info) to keep configurations isolated.

  3. 3

    Attach a FileHandler to each module logger to control where records are written, and attach a Formatter to the handler to control message formatting.

  4. 4

    Use logger.setLevel(...) to control what the logger emits, and use handler.setLevel(...) to control what each output actually records.

  5. 5

    Inside except blocks, prefer logging.exception(...) when you want tracebacks included automatically.

  6. 6

    Add multiple handlers (e.g., FileHandler plus StreamHandler) to send different subsets of logs to different destinations.

  7. 7

    Handler-level thresholds enable practical filtering, such as writing only ERROR and above to a specific log file while keeping DEBUG enabled elsewhere.

Highlights

Sharing the root logger across modules can prevent expected logs from being created or can filter out messages due to mismatched log levels.
Per-module loggers named with __name__ prevent configuration collisions and keep file outputs and formatting consistent.
Setting levels on handlers (not just loggers) enables targeted logging—like writing only errors to sample.log.
logging.exception inside an except block automatically includes the traceback, turning “what happened” into “where and why.”
Adding a StreamHandler alongside a FileHandler lets debug output appear in the console while errors still land in log files.

Topics

  • Python Logging
  • Loggers
  • Handlers
  • Formatters
  • Root Logger Pitfalls

Mentioned