Skip to main content

Python Logging: Stop Using print() for Debugging

Intermediate20 min5 exercises70 XP
0/5 exercises

Every airplane has a flight recorder — a "black box" that silently records everything that happens during a flight. If something goes wrong, investigators can replay the recording to figure out exactly what happened and when.

Python's logging module is the black box for your programs. Instead of scattering print() statements everywhere and then deleting them later, you write log messages that can be turned on or off, filtered by importance, and formatted however you want.

In this tutorial, you'll learn why print() is a poor debugging tool, how log levels work, and how to set up logging that actually helps you find bugs.

Why Is print() Bad for Debugging?

We've all done it. Something isn't working, so you throw in a print() to see what's going on. Then another. And another. Pretty soon your output is a wall of text and you can't tell the important messages from the noise.

The print() debugging problem
Loading editor...

See how the debug messages get mixed in with the real output? You'd have to delete or comment out every print() before sharing this code. And if the bug comes back, you'd have to add them all again.

print() debugging
# Must manually add and remove
print('DEBUG: entering function')
print('DEBUG: x =', x)
# Must delete before production
# No way to filter by severity
# No timestamps
logging module
# Turn on/off with one line
import logging
logging.debug('Entering function')
logging.debug('x = %s', x)
# Just change the log level
# Filter by severity
# Automatic timestamps

What Are Log Levels and When Should You Use Each?

Not all messages are equally important. A message saying "connected to database" is nice to know. A message saying "database is on fire" is urgent. Log levels let you categorize messages by importance.

Python has five built-in log levels, from least to most severe:

  • DEBUG — Detailed info for diagnosing problems. ("Variable x is 42.")
  • INFO — Confirmation that things are working. ("Server started on port 8080.")
  • WARNING — Something unexpected but not broken. ("Disk is 90% full.")
  • ERROR — Something failed. ("Could not save file.")
  • CRITICAL — The program might crash. ("Out of memory!")
  • All five log levels
    Loading editor...

    When you set a log level, Python shows that level and everything above it. If you set the level to WARNING, you'll see WARNING, ERROR, and CRITICAL messages — but DEBUG and INFO are silenced.

    Filtering by log level
    Loading editor...

    How Do You Set Up Basic Logging?

    The basicConfig() function is the quickest way to configure logging. You call it once at the top of your program. It sets the level, format, and where messages go.

    Setting up basicConfig
    Loading editor...

    The format string controls what each log line looks like. %(levelname)s inserts the level name, and %(message)s inserts your message. The force=True parameter ensures the config applies even if logging was already configured.

    How Do You Format Log Messages?

    A good log message answers three questions: when did it happen, how serious is it, and what happened. You control this with format codes.

    Here are the most useful format codes:

  • %(asctime)s — Timestamp (when)
  • %(levelname)s — Level name like DEBUG, INFO (how serious)
  • %(message)s — Your message (what happened)
  • %(name)s — Logger name
  • %(funcName)s — Function name where the log was called
  • Formatted log output
    Loading editor...

    The datefmt parameter controls how the timestamp looks. %H:%M:%S gives you hours:minutes:seconds. In production, you'd typically include the full date too.

    How Do You Use Logging Inside Functions?

    Logging really shines when you use it inside functions. It lets you trace how data flows through your program without cluttering the output that your users see.

    Logging inside functions
    Loading editor...

    Notice how the log messages tell the story of what happened. You can see exactly which path the code took and why. This is incredibly useful when debugging problems in larger programs.

    Here's a more complete example showing how logging helps trace a multi-step process.

    Tracing a multi-step process
    Loading editor...

    Practice Exercises

    Predict the Output: Log Level Filtering
    Predict Output

    What will this code print? Remember, when the level is set to WARNING, only WARNING and above are shown.

    import logging
    logging.basicConfig(level=logging.WARNING, format='%(levelname)s: %(message)s', force=True)
    
    logging.debug('step 1')
    logging.info('step 2')
    logging.warning('step 3')
    logging.error('step 4')
    Loading editor...
    Add Logging to a Function
    Write Code

    Write a function called divide(a, b) that divides two numbers and uses logging:

    1. Set up basicConfig with level=logging.DEBUG and format='%(levelname)s: %(message)s' and force=True

    2. At the start of the function, log a DEBUG message: 'Dividing %s by %s', a, b

    3. If b is 0, log an ERROR message: 'Division by zero!' and return None

    4. Otherwise, log an INFO message: 'Result: %s', result and return the result

    Then call print(divide(10, 2)) and print(divide(5, 0)).

    Loading editor...
    Fix the Bug: Missing Messages
    Fix the Bug

    This code should show all five log messages, but only some appear. Find and fix the bug so that all five messages are displayed.

    Expected output:

    DEBUG: Starting
    INFO: Processing
    WARNING: Slow query
    ERROR: Connection lost
    CRITICAL: System down
    Loading editor...
    Build a Login Validator with Logging
    Write Code

    Write a function check_login(username, password) with proper logging.

    1. Configure logging: level=DEBUG, format='%(levelname)s: %(message)s', force=True

    2. Log a DEBUG message: 'Login attempt for user: <username>'

    3. If username is empty, log WARNING: 'Empty username provided' and return False

    4. If password has fewer than 8 characters, log WARNING: 'Password too short for user: <username>' and return False

    5. Otherwise log INFO: 'Login successful for user: <username>' and return True

    Test with:

    print(check_login('alice', 'securepass123'))
    print(check_login('', 'pass'))
    print(check_login('bob', 'short'))
    Loading editor...
    Refactor: Replace print() with Logging
    Refactor

    Refactor this function to replace all print() debug statements with proper logging calls. Keep the final print() for user output.

    Original code:

    def process_scores(scores):
        print('DEBUG: Received scores:', scores)
        if not scores:
            print('ERROR: No scores provided')
            return None
        total = sum(scores)
        print('DEBUG: Total =', total)
        average = total / len(scores)
        print('INFO: Average calculated:', average)
        return average

    1. Set up logging with level=DEBUG, format='%(levelname)s: %(message)s', force=True

    2. Replace print DEBUGs with logging.debug()

    3. Replace print ERROR with logging.error()

    4. Replace print INFO with logging.info()

    5. Call print(process_scores([85, 92, 78, 90])) and print(process_scores([]))

    Loading editor...