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.
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.
| Benefit | Description |
|---|---|
| Simplicity | No additional tools or setup required |
| Speed | Quick to implement and modify |
| Universality | Works in any Python environment |
| Accessibility | No learning curve for beginners |
| Remote debugging | Works on systems without debugger access |
| Permanent traces | Can be left in code for production logging |
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
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
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}")
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
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}}
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
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)
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")
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)
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
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")
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
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)
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
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")
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}")
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']}")
| Location | Purpose |
|---|---|
| Function entry | Show function calls and parameters |
| Before critical operations | Verify preconditions |
| After critical operations | Verify postconditions |
| Inside loops | Track iteration progress |
| Conditional branches | Confirm execution path |
| Error conditions | Identify failure causes |
| Function exit | Show return values |
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
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}")
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}")
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
| Level | When to Use | Production Visibility |
|---|---|---|
| DEBUG | Detailed diagnostic information | Disabled |
| INFO | General informational messages | Enabled |
| WARNING | Warning messages | Enabled |
| ERROR | Error messages | Enabled |
| CRITICAL | Critical problems | Enabled |
Important
Always use “DEBUG:” prefix for temporary print statements during development, making them easy to find and remove. For production code, use Python’s logging module instead.
| Scenario | Print Debugging | Interactive 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 | - |
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.