A Python script may fail for many reasons.
You might misspell a variable. Your data may use the wrong format. A function may return an unexpected value. Sometimes, the script runs without crashing but produces incorrect results.
Random code changes rarely solve these problems. They can also create new bugs.
You need a clear debugging process instead. This process helps you locate the problem, understand its cause, and apply a safe fix.
This guide explains how to debug Python scripts step by step. It covers error messages, print statements, logging, breakpoints, testing, and common debugging mistakes.
What Does Debugging Mean in Python?
Debugging means finding and fixing problems in your code.
A bug can cause several types of trouble:
The program crashes.
The program returns incorrect results.
A function behaves differently than expected.
The script becomes slow or stops responding.
The program works with some inputs but fails with others.
Good debugging does more than remove an error message. It identifies the real cause.
For example, an IndexError tells you that a list position does not exist. However, the deeper problem may involve an empty list or an incorrect loop condition.
You must investigate both the error and its cause.
Start by Reproducing the Problem
You cannot debug a problem that you cannot repeat.
Run the script again using the same input, environment, and settings. Record what happens before the failure.
Ask these questions:
Which command starts the script?
Which input causes the error?
Does the problem happen every time?
Did the script work before?
What changed recently?
Does it fail on another computer?
Use the smallest input that still causes the bug. Smaller examples reduce distractions.
Suppose a script fails while processing 10,000 records. Test it with ten records. Then test each record separately.
This method can reveal the exact record that triggers the error.
Read the Complete Python Traceback
Python usually prints a traceback after an unhandled exception.
A traceback shows:
The exception type
The error message
The affected file
The line number
The function call path
Consider this example:
prices = [15, 25, 35]
print(prices[3])
Python raises an IndexError because the list contains three items. Their positions are 0, 1, and 2.
Start reading a traceback from the final line. It usually contains the clearest error description.
Next, move upward through the function calls. Look for the first line that belongs to your code.
Library files may appear in the traceback. Do not change library code immediately. Your script may have passed invalid data into the library.
Common Python Exceptions
Different exceptions provide different clues.
Exception Common Cause First Check
SyntaxError Invalid Python syntax Check brackets, colons, and indentation
NameError Unknown variable name Check spelling and variable scope
TypeError Wrong data type or operation Inspect the involved values
ValueError Correct type but invalid value Validate the input
IndexError Invalid list position Check the list length
KeyError Missing dictionary key Inspect available keys
AttributeError Missing object attribute Check the object type
FileNotFoundError Incorrect file path Confirm the working directory
Do not hide an exception before understanding it.
A broad except block can prevent a crash. However, it can also hide useful details.
Check the Simplest Possible Causes
Developers sometimes search for complex problems too early.
Check basic issues first:
Misspelled names
Incorrect indentation
Missing files
Wrong paths
Empty values
Unexpected data types
Incorrect environment variables
Missing packages
Wrong Python versions
For example, this path may work on one computer:
It may fail on another computer because that location does not exist.
Use configurable paths instead of fixed machine-specific paths.
Inspect Values with Print Statements
Print statements offer a fast debugging method.
Add them before the suspected failure:
def calculate_discount(price, rate):
print(“price:”, price)
print(“rate:”, rate)
return price * rate
These statements reveal the values that enter the function.
Print both values and types when needed:
print(“price:”, price, “type:”, type(price))
A value may look correct but use the wrong type.
For example:
price = “100”
This value contains digits, but it remains a string.
Use clear labels in every debugging message. A plain print(value) becomes confusing when many values appear.
Remove temporary print statements after solving the issue. For larger programs, use logging instead.
Use Assertions to Check Assumptions
An assertion checks whether a condition remains true.
def calculate_average(numbers):
assert numbers, “The numbers list cannot be empty”
return sum(numbers) / len(numbers)
This assertion stops the function when the list is empty.
Assertions help you test internal assumptions, such as:
A list should contain items.
A number should remain positive.
A function should return a specific type.
A user ID should not be empty.
Do not use assertions to validate untrusted user input. Python can disable assertions during optimized execution.
Use regular conditions and exceptions for required validation.
if not numbers:
raise ValueError(“The numbers list cannot be empty”)
Reduce the Script to a Smaller Example
Large scripts contain many possible failure points.
Copy the suspected code into a small test file. Remove unrelated functions, imports, and data.
Suppose an API response causes an error. Save one sample response and test only the parsing function.
print(sample_data[“user”][“name”])
A minimal example helps you answer one question:
Does the bug come from this code or another part?
Add removed sections back one at a time. The problem often returns after you restore the responsible section.
Use Python’s Built-In Breakpoint
Python provides the breakpoint() function.
It pauses the script and opens an interactive debugging session.
def calculate_total(items):
total = 0
for item in items:
breakpoint()
total += item[“price”]
return total
When Python reaches the breakpoint, you can inspect the current state.
Useful debugger commands include:
Command Purpose
p variable Print a variable
pp variable Print a formatted value
n Run the next line
s Step inside a function
c Continue execution
l Show nearby code
w Display the call stack
q Exit the debugger
You can also start the debugger from the command line:
python -m pdb script.py
This approach helps when you cannot edit the script easily.
Place breakpoints near the suspected problem. Avoid stepping through every line from the program’s beginning.
Track Program Activity with Logging
Logging works better than print statements in larger projects.
It records events, values, warnings, and failures.
import logging
def process_order(order):
logging.debug(“Processing order: %s”, order)
if “total” not in order:
logging.error(“Order has no total: %s”, order)
return
logging.info(“Order processed successfully”)
Python logging supports several standard levels:
DEBUG for detailed diagnostic information
INFO for normal program events
WARNING for unexpected conditions
ERROR for failed operations
CRITICAL for serious failures
Avoid recording passwords, access tokens, payment details, or private user data.
Logs should explain what happened without exposing sensitive information.
Check Function Inputs and Outputs
Many bugs occur when one function sends unexpected data to another.
Inspect each function boundary.
Confirm these points:
The function receives the expected values.
The input uses the correct type.
Required fields exist.
The return value matches the caller’s needs.
The function handles empty values.
Type hints can also clarify expected data.
Type hints do not enforce types by themselves. However, editors and type-checking tools can detect many mistakes earlier.
Test Edge Cases
A function may work with normal input but fail at its limits.
Test cases such as:
Empty strings
Empty lists
Zero
Negative numbers
Very large values
Missing dictionary keys
None
Duplicate records
Unexpected characters
Invalid dates
Consider this function:
def first_item(items):
return items[0]
It works with a populated list. It fails with an empty list.
A safer version checks the edge case:
def first_item(items):
if not items:
return None
return items[0]
Decide how each function should handle invalid input. Do not leave that behavior to chance.
Write a Test for Every Confirmed Bug
After finding a bug, create a test that reproduces it.
Run the test before applying the fix. It should fail.
Apply the fix and run it again. It should pass.
Keep the test in your test suite. It protects the code from the same bug later.
This process turns each bug into a permanent quality check.
Review Dependencies and the Runtime Environment
The code may be correct while the environment remains wrong.
Check:
Python version
Installed package versions
Virtual environment
Operating system
File permissions
Environment variables
Database settings
Working directory
Display the Python version with:
python –version
List installed packages with:
python -m pip freeze
Use a virtual environment for each project. It prevents packages from different projects from interfering with each other.
Document required package versions in a dependency file.
A Practical Python Debugging Checklist
Step Action Result
1 Reproduce the problem Confirms when the bug appears
2 Read the final traceback line Identifies the exception
3 Find the first relevant code line Locates the likely failure
4 Inspect inputs and variable types Reveals unexpected data
5 Add focused print statements or logs Tracks program flow
6 Reduce the failing code Removes unrelated distractions
7 Pause execution with breakpoint() Inspects live program state
8 Test empty and unusual values Finds edge-case failures
9 Check packages and environment Detects setup differences
10 Write a regression test Prevents the bug from returning
11 Remove temporary debugging code Keeps the project clean
12 Document the root cause Helps future maintenance
Common Debugging Mistakes to Avoid
Changing Several Things at Once
Change one item and test again.
Multiple changes hide which fix worked. They can also introduce new problems.
Ignoring the First Error
One failure can trigger several later errors.
Fix the earliest meaningful error first. The remaining errors may disappear.
Using Broad Exception Handling
Avoid code like this:
try:
process_data()
except Exception:
pass
It hides the exception and removes useful evidence.
Catch specific exceptions instead:
try:
process_data()
except ValueError as error:
Assuming the Input Is Correct
External files, APIs, forms, and databases can return unexpected data.
Validate input before processing it.
Leaving Debugging Code in Production
Temporary breakpoints can stop a live application. Print statements can expose private data.
Review all debugging changes before deployment.
Build a Repeatable Debugging Process
Strong debugging depends on evidence.
Start with the error message. Reproduce the failure. Inspect the data near the affected line. Reduce the code when the cause remains unclear.
Use print statements for quick checks. Use logging for larger applications. Use breakpoints when you need to inspect execution closely.
Then write a test for the confirmed bug.
A repeatable process saves time and reduces risky guesses. It also helps you understand your code more deeply.
Resources such as terribleanalogies.com can support developers who want clearer explanations of programming concepts and technical problems.