Master Python Debugging: From Print Statements to Advanced Error Handling
This comprehensive guide explores Python debugging and error handling techniques—from simple print statements and logging to pdb, structured logging, retry mechanisms, circuit breakers, testing strategies, and remote debugging—providing a complete toolkit for building robust and maintainable Python applications.
Introduction
Effective debugging and error handling are essential for building robust Python applications. This guide walks through basic print debugging, the logging module, the pdb debugger, advanced techniques, structured logging, retry mechanisms, circuit breaker pattern, testing strategies, and remote debugging.
Basic Debugging
Print debugging
def calculate_discount(price, discount_rate):
print(f"[DEBUG] price={price}, discount_rate={discount_rate}")
if discount_rate < 0 or discount_rate > 1:
print(f"[WARNING] Invalid discount_rate: {discount_rate}")
return price
discounted_price = price * (1 - discount_rate)
print(f"[DEBUG] result={discounted_price}")
return discounted_priceLogging
import logging
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
logger.debug("Starting business logic")
# ... use logger.info, logger.error, etc.Exception Handling
Try‑except structure
def read_file_safely(filename):
try:
with open(filename, 'r', encoding='utf-8') as file:
return file.read()
except FileNotFoundError:
print(f"Error: file {filename} not found")
return None
except PermissionError:
print(f"Error: no permission to read {filename}")
return None
except Exception as e:
print(f"Unknown error: {e}")
return NoneCustom exception
class ValidationError(Exception):
def __init__(self, message, field_name=None, value=None):
self.message = message
self.field_name = field_name
self.value = value
super().__init__(self.message)
def __str__(self):
if self.field_name:
return f"{self.field_name} validation failed: {self.message} (value: {self.value})"
return self.messageAdvanced Debugging
pdb and breakpoint()
import pdb
pdb.set_trace()
# or Python 3.7+
breakpoint()Third‑party tools (ipdb)
import ipdb
ipdb.set_trace()Logging Best Practices
Complete logging configuration
LOGGING_CONFIG = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'detailed': {'format': '%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'},
'simple': {'format': '%(levelname)s: %(message)s'}
},
'handlers': {
'console': {'class': 'logging.StreamHandler', 'level': 'INFO', 'formatter': 'simple'},
'file': {'class': 'logging.FileHandler', 'level': 'DEBUG', 'formatter': 'detailed', 'filename': 'app.log', 'mode': 'a'},
'error_file': {'class': 'logging.FileHandler', 'level': 'ERROR', 'formatter': 'detailed', 'filename': 'errors.log', 'mode': 'a'}
},
'loggers': {
'': {'handlers': ['console', 'file', 'error_file'], 'level': 'DEBUG', 'propagate': True},
'app.module': {'handlers': ['file'], 'level': 'DEBUG', 'propagate': False}
}
}
logging.config.dictConfig(LOGGING_CONFIG)Error‑Handling Patterns
Retry decorator
def retry(max_attempts=3, delay=1, backoff=2, exceptions=(Exception,)):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
attempts, current_delay = 0, delay
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except exceptions as e:
attempts += 1
if attempts == max_attempts:
raise
time.sleep(current_delay)
current_delay *= backoff
return wrapper
return decoratorCircuit breaker
class CircuitBreaker:
def __init__(self, failure_threshold=3, reset_timeout=10):
self.failure_threshold = failure_threshold
self.reset_timeout = reset_timeout
self.failures = 0
self.last_failure_time = None
self.state = "CLOSED"
# methods can_execute, record_success, record_failure ...Testing and Debugging Integration
Assertions
def process_data(data):
assert data is not None, "Data cannot be None"
assert isinstance(data, list), "Data must be a list"
assert len(data) > 0, "Data cannot be empty"
# processing logic
return resultUnit tests with debugging
import unittest
class TestDebugging(unittest.TestCase):
def test_error_handling(self):
with self.assertRaises(ValueError):
raise ValueError("test error")Production Debugging
Remote debugging with debugpy
import debugpy
debugpy.listen(("0.0.0.0", 5678))
print("Waiting for debugger...")
debugpy.wait_for_client()
print("Debugger attached!")Checklist
✅ Add appropriate assertions
✅ Handle possible exceptions
✅ Sufficient logging
✅ Unit test coverage
✅ User‑friendly error messages
✅ No sensitive data in logs
✅ Proper log levels for production
✅ Monitoring and alerting
✅ Effective recovery strategies
Conclusion
Combining systematic debugging, structured logging, robust exception handling, and automated testing creates resilient Python programs that are easier to maintain and scale.
Python Crawling & Data Mining
Life's short, I code in Python. This channel shares Python web crawling, data mining, analysis, processing, visualization, automated testing, DevOps, big data, AI, cloud computing, machine learning tools, resources, news, technical articles, tutorial videos and learning materials. Join us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
