Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
151: Configuring the logging Module
I've spent a lot of time reviewing PRs from junior devs, and there is one mistake that appears almost every single time someone starts using the logging module: they treat logging.basicConfig() like a setter method that they can call whenever they want to change how the program logs.
The Myth: Calling basicConfig repeatedly updates your settings
You'll often see code that looks like this. The developer thinks they can switch from a "general" mode to a "debug" mode halfway through the script by calling the configuration function again.
import logging
# Start with a basic setup
logging.basicConfig(level=logging.INFO)
logging.info("System is starting up...")
# Now we've hit a complex part of the code, let's turn on DEBUG logs!
logging.basicConfig(level=logging.DEBUG)
logging.debug("Checking the internal state of the data buffer...")
# Result: The debug message never prints.
Here is the catch: basicConfig does absolutely nothing if the root logger already has handlers configured. Since the first call created a handler (the default stream to stderr), the second call is simply ignored by Python. It doesn't throw an error; it just silently fails to do anything. This leads to hours of frustration where you're staring at your code wondering why your debug logs aren't appearing.
The Reality: Managing the Root Logger and its Handlers
To actually control your logging, you need to understand that logging is a hierarchy. At the top is the "root logger." When you call logging.info(), you're actually calling a convenience method that tells the root logger to handle the message.
If you need to change the log level on the fly, or if you want your logs to go to both a file and the console, you have to talk to the logger object directly or manipulate its handlers. I personally prefer creating a named logger for every module, but for this lesson, let's look at how to properly configure the root logger for a data ingestion script.
import logging
# Set up the root logger once
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG) # Set the lowest level you'll possibly need
# Create a console handler for general info
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# Create a file handler for detailed debugging
file_handler = logging.FileHandler('ingestion_debug.log')
file_handler.setLevel(logging.DEBUG)
# Add them both to the root logger
root_logger.addHandler(console_handler)
root_logger.addHandler(file_handler)
logging.info("Ingestion started.") # Goes to console and file
logging.debug("Connecting to API endpoint 0x445...") # Goes ONLY to file
By separating the Logger (the entry point) from the Handler (the destination), you gain total control. The logger acts as a gatekeeper; if the logger's level is set to INFO, a DEBUG message will be dropped before it even reaches the handlers. That's why I set the root logger to DEBUG in the example above—it opens the gate wide, allowing the individual handlers to decide what they actually want to keep.
One quick tip: if you're building a library that other people will use, never call basicConfig(). If you do, you're forcing your logging preferences on the person using your library, which is a great way to make them hate your package. Instead, just use logger = logging.getLogger(__name__) and let the end-user configure the root logger in their main script.
📋 Practical Task
Exercise: Building a Dual-Destination Audit Logger
You are building a financial transaction processor. For security and auditing reasons, you need to implement a logging system with the following strict requirements:
- All
WARNINGandERRORlogs must be written to a file namedaudit_trail.log. - All
INFOand higher logs must be printed to the console. DEBUGlogs should be completely ignored by both the console and the file to avoid leaking sensitive memory addresses in production.
Your Task: Write a script that configures the root logger to satisfy these requirements. After configuration, trigger one debug(), one info(), and one error() message. Verify that the debug message appears nowhere, the info message appears only in the console, and the error message appears in both.
There are no comments for now.