Debug With Print

This document covers debugging Python programs using print statements including strategies for variable inspection, execution flow tracking formatted output techniques, and best practices for effective printf debugging. Simple yet powerful debugging technique.

This document explores debugging Python programs using print statements, demonstrating how to inspect variables, track execution flow, format output effectively, and apply best practices for quick problem diagnosis without requiring complex debugging tools.


Introduction

Print debugging (also called printf debugging) is one of the simplest and most widely used debugging techniques. By strategically placing print statements in code, developers can observe program behavior, inspect variable values, and track execution flow to identify bugs quickly.


Why Use Print Debugging

Advantages of Print Statements

BenefitDescription
SimplicityNo additional tools or setup required
SpeedQuick to implement and modify
UniversalityWorks in any Python environment
AccessibilityNo learning curve for beginners
Remote debuggingWorks on systems without debugger access
Permanent tracesCan be left in code for production logging

When to Use Print Debugging

  • Quick verification of variable values
  • Checking if code blocks are executed
  • Validating function inputs and outputs
  • Tracing execution flow through complex logic
  • Debugging scripts running on remote servers
  • Initial problem exploration before using debuggers

Basic Print Debugging

Inspecting Variable Values

The most common use of print debugging is checking variable contents:

 1def calculate_total(items):
 2    total = 0
 3    for item in items:
 4        print(f"DEBUG: item = {item}")  # Inspect each item
 5        total += item['price']
 6        print(f"DEBUG: Running total = {total}")  # Track accumulation
 7    return total
 8
 9items = [
10    {'name': 'Widget', 'price': 10.50},
11    {'name': 'Gadget', 'price': 25.00}
12]
13
14result = calculate_total(items)
15print(f"Final total: {result}")

Output:

1DEBUG: item = {'name': 'Widget', 'price': 10.5}
2DEBUG: Running total = 10.5
3DEBUG: item = {'name': 'Gadget', 'price': 25.0}
4DEBUG: Running total = 35.5
5Final total: 35.5

Tracking Execution Flow

Use print statements to verify which code paths are executed:

 1def process_order(order):
 2    print("DEBUG: Entering process_order")
 3
 4    if order['status'] == 'pending':
 5        print("DEBUG: Order is pending")
 6        validate_order(order)
 7    elif order['status'] == 'confirmed':
 8        print("DEBUG: Order is confirmed")
 9        ship_order(order)
10    else:
11        print(f"DEBUG: Unknown status: {order['status']}")
12        return False
13
14    print("DEBUG: Exiting process_order")
15    return True

Advanced Print Techniques

Using f-strings for Formatted Output

Python’s f-strings provide powerful formatting capabilities:

 1def analyze_data(data):
 2    print(f"DEBUG: Processing {len(data)} items")
 3
 4    for idx, item in enumerate(data):
 5        print(f"DEBUG: [{idx}] {item['name']}: ${item['price']:.2f}")
 6
 7    total = sum(item['price'] for item in data)
 8    average = total / len(data)
 9
10    print(f"DEBUG: Total=${total:.2f}, Average=${average:.2f}")

Printing Variable Types

Inspect both value and type to catch type-related bugs:

 1def process_input(value):
 2    print(f"DEBUG: value={value}, type={type(value)}")
 3
 4    if isinstance(value, str):
 5        print("DEBUG: Processing as string")
 6        result = value.upper()
 7    elif isinstance(value, int):
 8        print("DEBUG: Processing as integer")
 9        result = value * 2
10    else:
11        print(f"DEBUG: Unexpected type: {type(value)}")
12        result = None
13
14    print(f"DEBUG: result={result}, type={type(result)}")
15    return result

Printing Data Structures

Use pprint module for readable output of complex structures:

 1import pprint
 2
 3def debug_config(config):
 4    print("DEBUG: Configuration:")
 5    pprint.pprint(config, indent=2, width=80)
 6
 7config = {
 8    'database': {
 9        'host': 'localhost',
10        'port': 5432,
11        'credentials': {'user': 'admin', 'password': 'secret'}
12    },
13    'features': ['auth', 'logging', 'caching'],
14    'limits': {'max_connections': 100, 'timeout': 30}
15}
16
17debug_config(config)

Output:

1DEBUG: Configuration:
2{ 'database': { 'credentials': {'password': 'secret', 'user': 'admin'},
3                'host': 'localhost',
4                'port': 5432},
5  'features': ['auth', 'logging', 'caching'],
6  'limits': {'max_connections': 100, 'timeout': 30}}

Debugging Common Issues

Finding IndexError Sources

 1def get_item(items, index):
 2    print(f"DEBUG: Accessing items[{index}], list length={len(items)}")
 3
 4    if index >= len(items):
 5        print(f"DEBUG: ERROR - Index {index} out of range!")
 6        return None
 7
 8    item = items[index]
 9    print(f"DEBUG: Retrieved item: {item}")
10    return item
11
12data = ['apple', 'banana', 'cherry']
13result = get_item(data, 5)  # Will show index out of range before crash

Debugging KeyError in Dictionaries

 1def get_user_email(user):
 2    print(f"DEBUG: User keys: {list(user.keys())}")
 3
 4    if 'email' not in user:
 5        print("DEBUG: WARNING - 'email' key not found!")
 6        return None
 7
 8    email = user['email']
 9    print(f"DEBUG: Email: {email}")
10    return email
11
12user = {'name': 'John', 'age': 30}  # Missing 'email'
13result = get_user_email(user)

Tracking Loop Iterations

 1def process_items(items):
 2    print(f"DEBUG: Starting loop with {len(items)} items")
 3
 4    for i, item in enumerate(items):
 5        print(f"DEBUG: Iteration {i+1}/{len(items)}")
 6        print(f"DEBUG:   Item: {item}")
 7
 8        if item['quantity'] <= 0:
 9            print(f"DEBUG:   Skipping - invalid quantity")
10            continue
11
12        result = item['price'] * item['quantity']
13        print(f"DEBUG:   Calculated: {result}")
14
15    print("DEBUG: Loop completed")

Debugging Function Calls

Printing Function Entry and Exit

 1def calculate_discount(price, discount_rate):
 2    print(f"DEBUG: ENTER calculate_discount(price={price}, discount_rate={discount_rate})")
 3
 4    if discount_rate < 0 or discount_rate > 1:
 5        print(f"DEBUG: Invalid discount rate: {discount_rate}")
 6        result = price
 7    else:
 8        discount = price * discount_rate
 9        result = price - discount
10        print(f"DEBUG: Discount amount: {discount}")
11
12    print(f"DEBUG: EXIT calculate_discount() -> {result}")
13    return result
14
15final_price = calculate_discount(100, 0.15)

Debugging Nested Function Calls

 1def outer_function(x):
 2    print(f"DEBUG: [outer_function] Called with x={x}")
 3    result = inner_function(x * 2)
 4    print(f"DEBUG: [outer_function] Returning {result}")
 5    return result
 6
 7def inner_function(y):
 8    print(f"DEBUG:   [inner_function] Called with y={y}")
 9    computed = y + 10
10    print(f"DEBUG:   [inner_function] Returning {computed}")
11    return computed
12
13output = outer_function(5)

Output:

1DEBUG: [outer_function] Called with x=5
2DEBUG:   [inner_function] Called with y=10
3DEBUG:   [inner_function] Returning 20
4DEBUG: [outer_function] Returning 20

Conditional Debug Printing

Using Debug Flags

 1DEBUG = True  # Set to False to disable debug output
 2
 3def debug_print(message):
 4    if DEBUG:
 5        print(f"DEBUG: {message}")
 6
 7def process_data(data):
 8    debug_print(f"Processing {len(data)} items")
 9
10    for item in data:
11        debug_print(f"Item: {item}")
12        # Process item...
13
14    debug_print("Processing complete")

Using Environment Variables

1import os
2
3DEBUG = os.environ.get('DEBUG', 'false').lower() == 'true'
4
5def debug_print(message):
6    if DEBUG:
7        print(f"DEBUG: {message}")
8
9# Run with: DEBUG=true python script.py

Function-Level Debug Control

 1def calculate_total(items, debug=False):
 2    if debug:
 3        print(f"DEBUG: Starting with {len(items)} items")
 4
 5    total = 0
 6    for item in items:
 7        if debug:
 8            print(f"DEBUG: Adding {item['price']}")
 9        total += item['price']
10
11    if debug:
12        print(f"DEBUG: Final total: {total}")
13
14    return total
15
16# Enable debugging for specific call
17result = calculate_total(items, debug=True)

Debugging with Timestamps

Adding Timestamps to Debug Output

 1import datetime
 2
 3def debug_print(message):
 4    timestamp = datetime.datetime.now().strftime('%H:%M:%S.%f')[:-3]
 5    print(f"[{timestamp}] DEBUG: {message}")
 6
 7def slow_operation():
 8    debug_print("Starting slow operation")
 9    # Simulate work
10    import time
11    time.sleep(2)
12    debug_print("Completed slow operation")
13
14slow_operation()

Output:

1[14:32:10.123] DEBUG: Starting slow operation
2[14:32:12.125] DEBUG: Completed slow operation

Measuring Execution Time

 1import time
 2
 3def measure_performance(func_name):
 4    def decorator(func):
 5        def wrapper(*args, **kwargs):
 6            start = time.time()
 7            print(f"DEBUG: [{func_name}] Starting...")
 8
 9            result = func(*args, **kwargs)
10
11            elapsed = time.time() - start
12            print(f"DEBUG: [{func_name}] Completed in {elapsed:.4f}s")
13            return result
14        return wrapper
15    return decorator
16
17@measure_performance("process_large_file")
18def process_file(filename):
19    # Processing logic
20    time.sleep(1.5)  # Simulate work
21    return True
22
23process_file("data.csv")

Best Practices for Print Debugging

Clear Debug Prefixes

Always use a consistent prefix like “DEBUG:” to easily identify and remove debug statements:

1# Good - Easy to find and remove
2print("DEBUG: Processing item")
3print(f"DEBUG: value = {x}")
4
5# Bad - Mixes with normal output
6print("Processing item")
7print(f"value = {x}")

Meaningful Context

Provide enough context to understand the output:

1# Good - Clear context
2print(f"DEBUG: calculate_total() - item[{i}]: price={item['price']}, qty={item['qty']}")
3
4# Bad - Unclear context
5print(f"DEBUG: {item['price']}, {item['qty']}")

Strategic Placement

LocationPurpose
Function entryShow function calls and parameters
Before critical operationsVerify preconditions
After critical operationsVerify postconditions
Inside loopsTrack iteration progress
Conditional branchesConfirm execution path
Error conditionsIdentify failure causes
Function exitShow return values

Clean Up After Debugging

 1# Option 1: Remove all debug statements after fixing issue
 2# Use grep to find: grep -r "DEBUG:" .
 3
 4# Option 2: Convert to proper logging
 5import logging
 6logging.basicConfig(level=logging.INFO)
 7
 8def process_data(data):
 9    logging.debug(f"Processing {len(data)} items")  # Only shows with DEBUG level
10    # Process data...
11    logging.info("Processing complete")  # Always shows

Practical Example: Debugging a Bug

Original Buggy Code

1def calculate_average(numbers):
2    total = sum(numbers)
3    average = total / len(numbers)
4    return average
5
6data = [10, 20, 30, 0, 50]
7result = calculate_average(data)
8print(f"Average: {result}")

Adding Debug Statements

 1def calculate_average(numbers):
 2    print(f"DEBUG: Input numbers: {numbers}")
 3    print(f"DEBUG: Number count: {len(numbers)}")
 4
 5    total = sum(numbers)
 6    print(f"DEBUG: Sum: {total}")
 7
 8    if len(numbers) == 0:
 9        print("DEBUG: WARNING - Empty list!")
10        return 0
11
12    average = total / len(numbers)
13    print(f"DEBUG: Average calculation: {total} / {len(numbers)} = {average}")
14
15    return average
16
17# Test with various inputs
18print("\nTest 1: Normal data")
19data1 = [10, 20, 30, 0, 50]
20result1 = calculate_average(data1)
21print(f"Result: {result1}\n")
22
23print("Test 2: Empty list")
24data2 = []
25result2 = calculate_average(data2)  # Would crash without debug fix
26print(f"Result: {result2}\n")
27
28print("Test 3: Single item")
29data3 = [42]
30result3 = calculate_average(data3)
31print(f"Result: {result3}")

Transition to Logging

From Print to Logging Module

For production code, convert debug prints to proper logging:

 1import logging
 2
 3# Configure logging
 4logging.basicConfig(
 5    level=logging.DEBUG,
 6    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
 7)
 8
 9logger = logging.getLogger(__name__)
10
11def process_order(order):
12    logger.debug(f"Processing order: {order['id']}")
13
14    if order['total'] > 1000:
15        logger.warning(f"Large order: ${order['total']}")
16
17    try:
18        submit_order(order)
19        logger.info(f"Order {order['id']} submitted successfully")
20    except Exception as e:
21        logger.error(f"Failed to submit order {order['id']}: {e}")
22        raise

Logging Levels

LevelWhen to UseProduction Visibility
DEBUGDetailed diagnostic informationDisabled
INFOGeneral informational messagesEnabled
WARNINGWarning messagesEnabled
ERRORError messagesEnabled
CRITICALCritical problemsEnabled

Comparison: Print vs Debugger

When to Use Each Approach

ScenarioPrint DebuggingInteractive Debugger
Quick value check✓ Better-
Complex state inspection-✓ Better
Remote system debugging✓ Better-
Step-by-step execution-✓ Better
Understanding code flow✓ Good✓ Better
Production debugging✓ Logging-
Learning new codebase✓ Good✓ Good
Time pressure✓ Faster-

Conclusion

Print debugging is a simple yet powerful technique for diagnosing Python program issues. By strategically placing print statements to inspect variables, track execution flow, and verify assumptions, developers can quickly identify bugs without complex debugging tools. Best practices include using clear prefixes, providing meaningful context, and transitioning to proper logging for production code. While interactive debuggers offer more sophisticated features, print debugging remains valuable for quick investigations, remote debugging, and initial problem exploration.


FAQ