exit
Key Takeaway: Python debugging doesn’t have to be intimidating. With the right tools and techniques—assertions, exception handling, logging, and systematic debugging approaches—you can quickly identify and fix issues in your code.
Author’s Note: Dear Reader, I want to be completely honest with you from the start: I am learning Python debugging as I write this series. This isn’t coming from someone who has mastered every aspect of Python development, it’s coming from someone who is actively working through these concepts, making mistakes, and discovering better ways to debug code.
P.S. - Keep a debugging journal! I wish I had started one earlier. Writing down the problems you solve helps you recognize patterns and builds your debugging intuition over time. Hopefully this blog series will serve as that.
Here is a list of links that will continue to grow and hopefully help out: Python on Dots
Understanding Python Debugging Fundamentals
Debugging is the process of finding and fixing errors (bugs) in your code . As a beginner Python programmer, you’ll encounter various types of errors that can seem overwhelming at first. However, with the right approach and tools, debugging becomes much more manageable.
Python provides several built-in tools and techniques to help you identify and resolve issues:
- Assertions for checking assumptions during development
- Exception handling for managing runtime errors gracefully
- Logging for tracking program execution and events
- Debug statements and interactive debugging tools
Let’s explore each of these techniques with practical, working examples.
Mastering Python Assertions
Assertions are statements that check if a condition is true at a specific point in your code . If the condition is false, Python raises an AssertionError
and stops execution, helping you catch bugs early in development.
Basic Assertion Syntax
assert condition, "Optional error message"
The assertion checks if the condition
is True
. If it’s False
, Python raises an AssertionError
with your optional message.
Working Example: Input Validation
def calculate_square_root(x):
"""Calculate square root with assertion check."""
assert x >= 0, "Input must be non-negative for square root"
return x ** 0.5
# Test successful case
print(f"Square root of 9 = {calculate_square_root(9)}") # Works fine
Square root of 9 = 3.0
# Test assertion failure
try:
-4) # This will raise AssertionError
calculate_square_root(except AssertionError as e:
print(f"AssertionError: {e}")
AssertionError: Input must be non-negative for square root
When to Use Assertions
Use Assertions For | Don’t Use Assertions For |
---|---|
Internal self-checks during development | Handling user input errors |
Verifying algorithm assumptions | Production error handling |
Checking data structure integrity | Validating external data |
Testing function preconditions | Runtime error management |
Practical Assertion Example: Data Validation
def process_student_grades(grades):
"""Process a list of student grades with validation."""
assert isinstance(grades, list), "Grades must be a list"
assert len(grades) > 0, "Grades list cannot be empty"
assert all(0 <= grade <= 100 for grade in grades), "All grades must be between 0 and 100"
= sum(grades) / len(grades)
average return round(average, 2)
# Valid case
= [85, 92, 78, 96, 88]
valid_grades print(f"Average grade: {process_student_grades(valid_grades)}")
Average grade: 87.8
# Invalid case (grade out of range)
try:
= [85, 92, 105, 78] # 105 is invalid
invalid_grades
process_student_grades(invalid_grades)except AssertionError as e:
print(f"Validation failed: {e}")
Validation failed: All grades must be between 0 and 100
Exception Handling: Managing Runtime Errors
Exception handling allows your program to respond to runtime errors gracefully instead of crashing . Python uses a try-except
structure to catch and handle different types of errors.
Common Python Exceptions
Exception Type | When It Occurs | Example |
---|---|---|
ValueError |
Invalid value for a function | int("abc") |
TypeError |
Operation on incompatible types | "a" + 1 |
ZeroDivisionError |
Division by zero | 10 / 0 |
IndexError |
List index out of range | 1, 2]| | KeyError| Dictionary key not found | [“missing”]| | FileNotFoundError| File doesn't exist | open(“missing.txt”)` |
Basic Exception Handling
def safe_divide(a, b):
"""Safely divide two numbers with exception handling."""
try:
= a / b
result return result
except ZeroDivisionError:
print(f"Error: Cannot divide {a} by zero!")
return None
except TypeError:
print(f"Error: Both arguments must be numbers")
return None
# Test cases
print(f"10 ÷ 2 = {safe_divide(10, 2)}") # Works: 5.0
10 ÷ 2 = 5.0
print(f"10 ÷ 0 = {safe_divide(10, 0)}") # Handles error gracefully
Error: Cannot divide 10 by zero!
10 ÷ 0 = None
print(f"'hi' ÷ 5 = {safe_divide('hi', 5)}") # Handles type error
Error: Both arguments must be numbers
'hi' ÷ 5 = None
Complete Exception Handling Structure
def read_file_safely(filename):
"""Demonstrate complete exception handling structure."""
try:
with open(filename, 'r') as file:
= file.read()
content return content
except FileNotFoundError:
print(f"Error: File '{filename}' not found")
return None
except PermissionError:
print(f"Error: Permission denied for '{filename}'")
return None
else:
# This runs only if no exception occurred
print(f"Successfully read {filename}")
finally:
# This always runs
print("File operation completed")
The try-except-else-finally
structure provides complete control: - try: Code that might raise an exception - except: Handle specific exceptions - else: Runs only if no exception occurs - finally: Always runs (cleanup code)
Python Logging: Better Than Print Statements
Logging is the process of recording events during program execution [[4]]. Unlike print statements, logging provides levels, timestamps, and flexible output options.
Logging Levels Explained
Level | Purpose | Example Use Case |
---|---|---|
DEBUG |
Detailed diagnostic information | Variable values, function calls |
INFO |
Confirmation things work as expected | Process completed successfully |
WARNING |
Something unexpected happened | Deprecated feature used |
ERROR |
Serious problem occurred | Database connection failed |
CRITICAL |
Very serious error | System crash imminent |
Basic Logging Setup
import logging
# Configure logging
logging.basicConfig(=logging.DEBUG,
levelformat='%(asctime)s - %(levelname)s - %(message)s',
='%H:%M:%S'
datefmt
)
# Create logger
= logging.getLogger(__name__)
logger
# Use different logging levels
"This is detailed debug information")
logger.debug("This confirms normal operation")
logger.info("This warns about unexpected events")
logger.warning("This reports serious problems")
logger.error("This reports critical system failures") logger.critical(
Logging in Functions: Practical Example
def calculate_factorial(n):
"""Calculate factorial with comprehensive logging."""
f"Starting factorial calculation for n={n}")
logger.info(
# Input validation with logging
if not isinstance(n, int):
f"Invalid input type: {type(n)}, expected int")
logger.error(return None
if n < 0:
f"Negative input not allowed: {n}")
logger.error(return None
if n > 20:
f"Large input {n} may cause overflow")
logger.warning(
f"Input validation passed for n={n}")
logger.debug(
# Calculate factorial
= 1
result for i in range(1, n + 1):
*= i
result f"Step {i}: result = {result}")
logger.debug(
f"Factorial calculation complete: {n}! = {result}")
logger.info(return result
# Test the function
= calculate_factorial(5)
factorial_5 print(f"5! = {factorial_5}")
5! = 120
Debugging Techniques and Strategies
Strategic Print Statement Debugging
While logging is preferred for production code, print statements are useful for quick debugging during development:
def find_maximum_in_list(numbers):
"""Find maximum with debug print statements."""
print(f"DEBUG: Starting with list = {numbers}")
if not numbers:
print("DEBUG: Empty list provided")
return None
= numbers[0]
max_value = 0
max_index
for i, value in enumerate(numbers):
print(f"DEBUG: Checking index {i}, value = {value}")
if value > max_value:
= value
max_value = i
max_index print(f"DEBUG: New maximum: {max_value} at index {max_index}")
print(f"DEBUG: Final result: max_value={max_value}, index={max_index}")
return max_value, max_index
# Test
= find_maximum_in_list([3, 7, 2, 9, 1, 5]) result
DEBUG: Starting with list = [3, 7, 2, 9, 1, 5]
DEBUG: Checking index 0, value = 3
DEBUG: Checking index 1, value = 7
DEBUG: New maximum: 7 at index 1
DEBUG: Checking index 2, value = 2
DEBUG: Checking index 3, value = 9
DEBUG: New maximum: 9 at index 3
DEBUG: Checking index 4, value = 1
DEBUG: Checking index 5, value = 5
DEBUG: Final result: max_value=9, index=3
print(f"Maximum: {result[0]} at position {result[1]}")
Maximum: 9 at position 3
Using Python’s Built-in Debugger (pdb)
Python’s pdb
module allows interactive debugging:
import pdb
def problematic_function(x, y):
# Execution will pause here
pdb.set_trace() = x * y
result = result / (x - y)
final_result return final_result
# When you run this, you can inspect variables interactively
Common pdb commands:
n
(next line)s
(step into function)c
(continue execution)p variable_name
(print variable value)q
(quit debugger)
Your Turn! Practice Exercise
Challenge: Create a simple bank account class that uses all the debugging techniques we’ve covered.
Requirements: 1. Use assertions to validate inputs 2. Handle exceptions for invalid operations 3. Add logging for all transactions 4. Include debug information for troubleshooting
import logging
class BankAccount:
def __init__(self, account_number, initial_balance=0):
# Your code here
pass
def deposit(self, amount):
# Your code here
pass
def withdraw(self, amount):
# Your code here
pass
def get_balance(self):
# Your code here
pass
# Test your implementation
= BankAccount("12345", 1000)
account 500)
account.deposit(200)
account.withdraw(print(f"Final balance: ${account.get_balance()}")
Final balance: $None
Click here for Solution!
import logging
# Configure logging
=logging.INFO,
logging.basicConfig(levelformat='%(asctime)s - %(levelname)s - %(message)s')
= logging.getLogger(__name__)
logger
class BankAccount:
def __init__(self, account_number, initial_balance=0):
# Use assertions to validate inputs
assert isinstance(account_number, str), "Account number must be a string"
assert len(account_number) > 0, "Account number cannot be empty"
assert isinstance(initial_balance, (int, float)), "Initial balance must be a number"
assert initial_balance >= 0, "Initial balance cannot be negative"
self.account_number = account_number
self.balance = initial_balance
f"Account {account_number} created with balance ${initial_balance}")
logger.info(
def deposit(self, amount):
f"Deposit request: ${amount} to account {self.account_number}")
logger.debug(
try:
# Validate input
assert isinstance(amount, (int, float)), "Deposit amount must be a number"
assert amount > 0, "Deposit amount must be positive"
# Process deposit
self.balance += amount
f"Deposited ${amount}. New balance: ${self.balance}")
logger.info(return True
except AssertionError as e:
f"Deposit failed: {e}")
logger.error(return False
except Exception as e:
f"Unexpected error during deposit: {e}")
logger.critical(return False
def withdraw(self, amount):
f"Withdrawal request: ${amount} from account {self.account_number}")
logger.debug(
try:
# Validate input
assert isinstance(amount, (int, float)), "Withdrawal amount must be a number"
assert amount > 0, "Withdrawal amount must be positive"
# Check sufficient funds
if amount > self.balance:
raise ValueError(f"Insufficient funds. Balance: ${self.balance}, Requested: ${amount}")
# Process withdrawal
self.balance -= amount
f"Withdrew ${amount}. New balance: ${self.balance}")
logger.info(return True
except (AssertionError, ValueError) as e:
f"Withdrawal failed: {e}")
logger.error(return False
except Exception as e:
f"Unexpected error during withdrawal: {e}")
logger.critical(return False
def get_balance(self):
f"Balance inquiry for account {self.account_number}")
logger.debug(return self.balance
# Test the implementation
try:
= BankAccount("12345", 1000)
account 500)
account.deposit(200)
account.withdraw(2000) # This should fail
account.withdraw(print(f"Final balance: ${account.get_balance()}")
except Exception as e:
f"Account creation failed: {e}") logger.critical(
True
True
False
Final balance: $1300
Quick Takeaways
• Assertions are your first line of defense against logic errors—use them to verify assumptions during development
• Exception handling prevents crashes by gracefully managing runtime errors with try-except blocks
• Logging is superior to print statements for tracking program execution—it provides levels, timestamps, and flexible output
• Strategic debugging involves reading error messages carefully, using print statements judiciously, and leveraging Python’s built-in debugger
• Always validate inputs and handle edge cases to make your code more robust
• Read error messages from bottom to top—the most relevant information is usually at the end
• Test your code incrementally rather than writing large chunks before testing
Debugging Techniques Comparison
Technique | Best Used For | When NOT to Use | Example |
---|---|---|---|
Assertions | Internal validation, algorithm invariants | Production error handling | assert x > 0, "Value must be positive" |
Exceptions | User input errors, file operations | Internal logic checks | try: ... except ValueError: ... |
Logging | Production monitoring, detailed tracking | Simple one-time debugging | logging.info("Process started") |
Print Statements | Quick debugging, temporary inspection | Production code | print(f"DEBUG: x = {x}") |
PDB Debugger | Complex bugs, step-by-step analysis | Simple issues | import pdb; pdb.set_trace() |
Common Python Errors and Solutions
Error Type | Common Cause | Prevention |
---|---|---|
SyntaxError |
Missing colons, incorrect indentation | Use IDE with syntax highlighting |
NameError |
Using undefined variables | Initialize variables before use |
TypeError |
Wrong data types in operations | Use type hints and validation |
ValueError |
Invalid values for functions | Add input validation |
IndexError |
List index out of range | Use len() to check bounds |
KeyError |
Dictionary key not found | Use dict.get() with defaults |
ZeroDivisionError |
Division by zero | Add zero checks before division |
Conclusion
Python debugging doesn’t have to be a frustrating experience. By mastering assertions, exception handling, logging, and systematic debugging approaches, you can quickly identify and resolve issues in your code.
Remember these key principles: - Use assertions to catch bugs early during development - Handle exceptions to make your programs robust and user-friendly
- Implement logging for better visibility into your program’s behavior - Debug systematically by reading error messages carefully and testing incrementally
The techniques covered in this guide will serve you well throughout your Python programming journey. As you practice and encounter more complex problems, these debugging skills will become second nature.
Ready to level up your Python debugging skills? Start by implementing these techniques in your current projects, and don’t forget to keep that debugging journal—you’ll be amazed at how much you learn from tracking the problems you solve!
Frequently Asked Questions
Q: Should I use assertions in production code? A: No, assertions can be disabled with Python’s -O
flag and should only be used during development for internal checks. Use proper exception handling for production error management.
Q: When should I use logging instead of print statements? A: Use logging when you need different severity levels, want to output to files, need timestamps, or are working on production code. Print statements are fine for quick debugging during development.
Q: What’s the difference between errors and exceptions in Python? A: In Python, “errors” and “exceptions” are often used interchangeably. Technically, exceptions are a type of error that can be caught and handled with try-except blocks.
Q: How do I read Python error messages effectively? A: Start from the bottom of the traceback and work your way up. The last line contains the error type and message, while preceding lines show the call stack that led to the error.
Q: Is it okay to use bare except clauses? A: No, avoid using except:
without specifying exception types. This can hide unexpected errors and make debugging harder. Always catch specific exceptions when possible.
Found this guide helpful? Share your debugging experiences in the comments below and let us know which technique you found most useful! Don’t forget to bookmark this page for future reference. 📚✨
References
Real Python Team. (2024). Python’s assert: Debug and test your code like a pro. Real Python. https://realpython.com/python-assert-statement/
Real Python Team. (2024, December 1). Python exceptions: An introduction. Real Python. https://realpython.com/python-exceptions/
Sweigart, A. (n.d.). Chapter 11: Debugging. In Automate the boring stuff with Python (2nd ed.). https://automatetheboringstuff.com/2e/chapter11/
W3Schools. (n.d.). Python assert keyword. W3Schools. https://www.w3schools.com/python/ref_keyword_assert.asp
Happy Coding! 🚀
You can connect with me at any one of the below:
Telegram Channel here: https://t.me/steveondata
LinkedIn Network here: https://www.linkedin.com/in/spsanderson/
Mastadon Social here: https://mstdn.social/@stevensanderson
RStats Network here: https://rstats.me/@spsanderson
GitHub Network here: https://github.com/spsanderson
Bluesky Network here: https://bsky.app/profile/spsanderson.com
My Book: Extending Excel with Python and R here: https://packt.link/oTyZJ
You.com Referral Link: https://you.com/join/EHSLDTL6