How to Debug Python Code Step-by-Step: The Complete Beginner’s Guide (2026)
Here’s a truth no one tells beginner programmers: even experienced developers write buggy code every single day.
The difference between a frustrated beginner and a confident developer is not that one writes perfect code and the other doesn’t. It’s that the confident developer knows exactly what to do when something breaks.
That skill is called debugging — and it’s arguably more important than writing code itself.
If you’ve ever stared at a red error message wondering what went wrong, or watched your program run without crashing but produce completely wrong results, you already know why debugging matters. Python is a powerful language, but it’s not magic — it does exactly what you tell it to do, even when what you told it is wrong.
This guide will walk you through how to debug Python code step-by-step, from reading your first error message to using professional-grade tools like pdb and IDE debuggers. By the end, you’ll have a complete, practical debugging toolkit you can use on any Python project.
💡 Before we begin: A lot of Python bugs come from how your program handles user input. If you haven’t already, check out our guide on how to take user input in Python — it covers one of the most common sources of beginner bugs.
Here’s what you’ll learn in this guide:
- What debugging is and why it matters
- The three types of Python errors and how to spot each one
- How to read Python error messages and tracebacks
- Debugging with
print()statements (the beginner’s best friend) - Using Python’s built-in
pdbdebugger - Visual debugging in VS Code and PyCharm
- Setting breakpoints and stepping through code
- Handling exceptions and tracebacks properly
- Using the
loggingmodule for professional debugging - Common beginner debugging mistakes (and how to avoid them)
- Best practices and real-world debugging examples
Let’s get started.
What Is Debugging in Python?
Debugging is the process of identifying, analyzing, and removing errors — commonly called bugs — from your code.
The term “bug” has an interesting history. In 1947, computer scientist Grace Hopper and her team found an actual moth trapped inside a relay of the Harvard Mark II computer, causing a malfunction. They taped it into a logbook with the note: “First actual case of bug being found.” The term stuck, and today it refers to any error or unintended behavior in software.
In Python, debugging means:
- Reproducing the error reliably
- Locating where in the code the problem occurs
- Understanding why the problem happens
- Fixing the root cause (not just the symptom)
- Verifying that the fix works and hasn’t broken anything else
Debugging vs. Testing — What’s the Difference?
These two terms are related but different:
- Testing means verifying that your code works correctly before or after deployment
- Debugging means finding and fixing problems when something goes wrong
Think of testing as a health check and debugging as the diagnosis after a symptom appears.
The Debugging Mindset
The most important thing about debugging is to approach it systematically, not randomly. Don’t just change random things and hope something works. Instead:
- Ask: “What exactly is happening vs. what should be happening?”
- Ask: “When does this happen? Always, or only with certain inputs?”
- Make one small change at a time and test immediately
With that mindset established, let’s look at the types of errors you’ll encounter.
Types of Errors in Python
Python errors fall into three main categories. Each behaves differently and requires a different debugging approach. Understanding which type you’re dealing with is your first step toward fixing it.
Syntax Errors — Python Can’t Even Read Your Code
A syntax error occurs when Python encounters code that violates the language’s grammar rules. Think of it like a sentence that doesn’t make grammatical sense — Python simply cannot parse what you’ve written.
The key characteristic of syntax errors is that they are caught before the program runs. Your script won’t execute at all if there’s a syntax error.
Common causes:
- Missing colons after
if,for,while,def,classstatements - Unclosed parentheses, brackets, or quotes
- Incorrect indentation
- Using Python reserved keywords as variable names
Example:
# Missing colon after if statement
if x > 5
print("x is greater than 5")
Python’s response:
File "example.py", line 1
if x > 5
^
SyntaxError: expected ':'
Notice how Python tells you exactly which line has the problem and even points to the location with a caret (^). Syntax errors are actually the easiest to fix once you know what to look for.
Runtime Errors (Exceptions) — Your Code Crashes While Running
A runtime error (also called an exception) occurs while your program is actually running. Python understood your code, but something went wrong during execution.
Runtime errors can be tricky because they sometimes only appear under specific conditions — for example, only when a user enters certain input, or only when a file doesn’t exist.
Most common runtime errors in Python:
| Error Type | Cause | Example |
|---|---|---|
NameError | Using a variable that doesn’t exist | print(total) before defining total |
TypeError | Wrong data type for an operation | "age" + 25 |
IndexError | Accessing a list index that doesn’t exist | my_list[10] on a 5-item list |
ZeroDivisionError | Dividing by zero | 10 / 0 |
KeyError | Accessing a dictionary key that doesn’t exist | my_dict["missing_key"] |
AttributeError | Calling a method that doesn’t exist on an object | "hello".uppercase() |
FileNotFoundError | Trying to open a file that doesn’t exist | open("data.txt") |
ValueError | Correct type but invalid value | int("abc") |
Example:
my_list = [1, 2, 3]
print(my_list[10]) # IndexError: list index out of range
The good news: runtime errors generate a traceback — Python’s crash report — that tells you exactly where things went wrong.
Logical Errors — The Code Runs, But Gives Wrong Answers
Logical errors are the sneakiest category. Your program runs without crashing, doesn’t produce any error messages, but the output is simply wrong.
These are caused by flaws in your reasoning — a wrong formula, an incorrect condition, a loop that goes one iteration too many or too few. Because Python has no way to know what you intended to do, it cannot flag these automatically.
Example:
# Trying to calculate average of two numbers
x = 10
y = 20
average = x + y / 2 # Logical error: should be (x + y) / 2
print(average) # Prints 20.0, not 15.0
The code runs perfectly — but the answer is wrong because of operator precedence. This is a logical error.
How to find logical errors:
- Add
print()statements to inspect intermediate values - Use a debugger to step through the code line by line
- Write test cases with known inputs and expected outputs
- Double-check every formula, condition, and algorithm
Understanding Python Error Messages and Tracebacks
When Python encounters a runtime error, it doesn’t just crash silently. It generates a traceback — a detailed report of what happened and where.
Learning to read tracebacks is one of the most valuable debugging skills you can develop. Most beginners panic when they see a red wall of text. Instead, train yourself to read it calmly and systematically.
Anatomy of a Python Traceback
Let’s look at a real example:
def calculate_discount(price, discount):
return price - (price * discount / 100)
def apply_to_cart(cart):
total = 0
for item in cart:
total += calculate_discount(item["price"], item["discont"]) # Typo!
return total
my_cart = [{"price": 100, "discount": 10}]
print(apply_to_cart(my_cart))
Python’s traceback:
Traceback (most recent call last):
File "shop.py", line 11, in <module>
print(apply_to_cart(my_cart))
File "shop.py", line 7, in apply_to_cart
total += calculate_discount(item["price"], item["discont"])
KeyError: 'discont'
Here’s how to read it:
| Part | What It Means |
|---|---|
Traceback (most recent call last): | Python is about to show you the call chain |
File "shop.py", line 11, in <module> | The error started from line 11 in the main script |
File "shop.py", line 7, in apply_to_cart | Which then called this function |
total += calculate_discount(item["price"], item["discont"]) | The exact line that caused the problem |
KeyError: 'discont' | ← START HERE. This is the actual error |
The Golden Rule: Read Tracebacks Bottom-Up
Always start at the last line. The final line of a traceback tells you:
- The type of error (
KeyError) - The detail of what went wrong (
'discont'— a misspelled key)
Once you know the error type and message, trace upward through the call stack to find where in your code it originated.
In the example above, the fix is simple: change item["discont"] to item["discount"].
💡 Pro Tip: Python 3.10 and later versions have significantly improved error messages. They now include helpful suggestions like “Did you mean
discount?” making it even easier to spot typos.
📂 If you’re working with files and hitting tracebacks like
FileNotFoundError, our guide on how to read and write text files in Python explains file path handling and common file-related errors in detail.
Debugging with print() Statements
Before we talk about fancy debuggers, let’s talk about the tool every Python programmer reaches for first: the humble print() statement.
Print debugging means strategically adding print() calls to your code to see what’s happening at different points in execution. It’s simple, requires no setup, and works in any environment.
How to Use print() for Debugging Effectively
1. Print variable values at key checkpoints
def calculate_total(prices):
total = 0
for price in prices:
print(f"[DEBUG] Current price: {price}, Running total: {total}")
total += price
return total
result = calculate_total([10, 20, 30])
print(f"Final total: {result}")
Output:
[DEBUG] Current price: 10, Running total: 0
[DEBUG] Current price: 20, Running total: 10
[DEBUG] Current price: 30, Running total: 30
Final total: 60
Notice the [DEBUG] label — this makes it easy to identify your debugging output vs. your program’s actual output.
2. Print variable types, not just values
Sometimes the bug isn’t the value itself — it’s the type:
user_input = input("Enter a number: ")
print(f"Value: {user_input}, Type: {type(user_input)}")
# Output: Value: 5, Type: <class 'str'>
This reveals that input() always returns a string, not an integer — a classic beginner mistake.
3. Print function entry and exit points
def process_data(data):
print(f"[DEBUG] process_data called with: {data}")
result = data * 2
print(f"[DEBUG] process_data returning: {result}")
return result
4. Use f-strings for clean, readable debug output
x, y, z = 10, 20, 30
print(f"[DEBUG] {x=}, {y=}, {z=}")
# Output: [DEBUG] x=10, y=20, z=30
The variable= syntax inside f-strings (Python 3.8+) automatically prints both the variable name and its value — incredibly useful for debugging.
When print() Is Enough (And When It Isn’t)
print() debugging is great for:
- Quick checks on small scripts
- Tracing a single variable through a loop
- Confirming whether a function is being called
It becomes a problem when:
- You have deeply nested code with many variables
- You forget to remove your print statements before sharing code
- You’re working on production applications
For those situations, Python’s proper debugging tools are a better choice.
Using Python Debugger (pdb)
The pdb module is Python’s built-in interactive debugger. Unlike print(), pdb lets you pause your program at any point, inspect all variables, and step through your code one line at a time.
You don’t need to install anything — pdb is part of Python’s standard library.
How to Start pdb
Method 1: Add a breakpoint directly in your code (Recommended)
def calculate_factorial(n):
result = 1
for i in range(1, n): # Bug: should be range(1, n + 1)
breakpoint() # Execution pauses here
result *= i
return result
print(calculate_factorial(5))
breakpoint() was introduced in Python 3.7 and is the modern, recommended way to invoke pdb. It’s cleaner than the older import pdb; pdb.set_trace() approach.
Method 2: Run your script through pdb from the terminal
python -m pdb your_script.py
This starts the debugger from the very first line of your script.
Essential pdb Commands
When pdb pauses your program, you’ll see the (Pdb) prompt. Here are the commands you’ll use most:
| Command | Short Form | What It Does |
|---|---|---|
next | n | Execute the next line (don’t enter functions) |
step | s | Step into the next function call |
continue | c | Run until the next breakpoint |
list | l | Show the code around the current line |
print variable | p variable | Print a variable’s value |
break line_number | b 10 | Set a breakpoint at line 10 |
quit | q | Exit the debugger |
where | w | Show the current call stack |
up / down | Navigate the call stack |
Practical pdb Walkthrough
Let’s debug a broken function:
def find_average(numbers):
total = 0
for num in numbers:
total = total + num
average = total / len(numbers)
return average
scores = [85, 90, 78, 92, 88]
print(find_average(scores))
This works fine — but what if we pass an empty list?
print(find_average([])) # ZeroDivisionError!
Add a breakpoint and investigate:
def find_average(numbers):
breakpoint() # Pause here
total = 0
for num in numbers:
total = total + num
average = total / len(numbers)
return average
print(find_average([]))
In the pdb session:
(Pdb) p numbers # Print the numbers variable
[]
(Pdb) p len(numbers) # Print the length
0
(Pdb) n # Next line
(Pdb) n # Skips the loop (empty list)
(Pdb) n # Now on: average = total / len(numbers)
You can now see the bug before the crash happens. Fix:
def find_average(numbers):
if not numbers:
return 0 # Handle empty list
total = sum(numbers)
return total / len(numbers)
Conditional Breakpoints
You can set breakpoints that only trigger under specific conditions:
# At the (Pdb) prompt:
(Pdb) b 5, i == 3 # Break at line 5 only when i equals 3
This is extremely useful for debugging loops — you can skip to the exact iteration where the bug appears.
For the complete official reference, see the Python pdb documentation.
Debugging with IDEs
While pdb is powerful, many developers prefer visual debugging inside an IDE (Integrated Development Environment). VS Code and PyCharm are the two most popular Python IDEs in 2026, and both have excellent debugging tools built right in.
Visual debugging lets you:
- Set breakpoints with a single click
- See all variables displayed in a panel — no typing required
- Visualize the call stack
- Evaluate expressions interactively
Debugging in VS Code
VS Code uses the Python Debugger extension (powered by debugpy) for debugging. It’s automatically installed when you add the Python extension.
Step 1: Set a breakpoint
Click in the gutter (the grey area to the left of the line numbers) next to the line where you want to pause. A red dot appears — that’s your breakpoint. You can also press F9 to toggle a breakpoint on the current line.
Step 2: Start debugging
Press F5 or go to Run → Start Debugging. If prompted, select “Python File” as the debug configuration.
Step 3: Use the debugging controls
When your code hits a breakpoint, execution pauses and the Debug toolbar appears:
| Button | Shortcut | Action |
|---|---|---|
| Continue | F5 | Run to the next breakpoint |
| Step Over | F10 | Execute current line, skip into functions |
| Step Into | F11 | Enter the function being called |
| Step Out | Shift+F11 | Finish current function, return to caller |
| Stop | Shift+F5 | Stop debugging |
Step 4: Inspect your variables
The Variables panel (on the left) automatically shows all local and global variables at the current breakpoint. No need to type anything.
The Debug Console at the bottom lets you type Python expressions and evaluate them against the current state of your program. This is incredibly powerful for testing fixes on the fly.
Pro Tip: Right-click on any breakpoint to add a condition, like total > 100. The debugger will only pause when that condition is true.
Debugging in PyCharm
PyCharm offers some of the most advanced debugging features available in any Python IDE.
Step 1: Set a breakpoint
Click in the left margin next to a line number. A red dot appears.
Step 2: Run in Debug mode
Click the green bug icon in the toolbar, or press Shift+F9.
Step 3: Use the Debugger panel
PyCharm’s debugger panel shows:
- Frames: The current call stack
- Variables: All variables and their values, updated live
- Watches: Variables or expressions you want to track throughout execution
Step 4: The Inline Debugger
One of PyCharm’s standout features is inline debugging — it displays variable values in light italic text directly in your code editor as you step through. You can see x = 42 appear right next to the line that sets x, without switching panels.
Step 5: Evaluate Expression
Press Alt+F8 (or Option+F8 on Mac) to open the Evaluate Expression window. You can type any Python expression — like len(my_list) or user.name.upper() — and see the result instantly without changing your code.
Step 6: Step Into My Code
Use the Step Into My Code button to avoid accidentally stepping into Python library code. This keeps your focus on your code rather than the internal implementation of modules you’re using.
For the official PyCharm debugging reference, see the JetBrains PyCharm Debugging Documentation.
Breakpoints and Step-by-Step Execution
If the IDE debugging section left you wondering what exactly a breakpoint does, this section is for you.
What Is a Breakpoint?
A breakpoint is a marker you place in your code that tells the debugger: “When you reach this line, pause execution and let me look around.”
Think of it like pressing pause on a video — your program freezes at that exact frame, and you can examine everything: variables, the call stack, memory — before pressing play again.
Types of Breakpoints
1. Regular Breakpoints The most common type. Pauses execution every time that line is reached.
2. Conditional Breakpoints Pauses execution only when a specific condition is true.
# Only pause when the loop counter hits 50
# (Set in IDE by right-clicking the breakpoint)
# Condition: i == 50
for i in range(1000):
process_item(i)
This is invaluable for debugging loops — you don’t want to step through 999 iterations to reach the one that fails.
3. Exception Breakpoints Pauses execution whenever a specific exception is raised, even if it’s caught by a try/except block. Available in both VS Code and PyCharm.
Step Execution — Your Three Key Moves
Once paused at a breakpoint, you control execution with three primary actions:
Step Over (F10 in VS Code, F8 in PyCharm) Execute the current line and move to the next one. If the current line calls a function, that function runs completely without stepping inside it.
Use when: You trust that the function works and just want to see what it returns.
Step Into (F11 in VS Code, F7 in PyCharm) If the current line calls a function, enter that function and pause at its first line.
Use when: You think the bug might be inside a function you wrote.
Step Out (Shift+F11 in VS Code, Shift+F8 in PyCharm) Complete the current function and return to wherever it was called from.
Use when: You accidentally stepped into a function and want to get back to the caller.
Practical Example: Debugging a Loop with Breakpoints
def find_maximum(numbers):
max_val = numbers[0]
for i in range(len(numbers)):
if numbers[i] > max_val:
max_val = numbers[i]
return max_val
data = [3, 7, 2, 9, 4, 1]
print(find_maximum(data)) # Should print 9
Set a breakpoint on the if numbers[i] > max_val: line, then use Step Over to watch max_val update on each iteration. You can verify that the variable correctly tracks the maximum at every step.
🎮 Practice Tip: The best way to learn breakpoint debugging is on a small project. Try building and intentionally breaking our Guess the Number game in Python — it’s a perfect project for practicing loop and conditional debugging.
Handling Exceptions and Errors
Debugging isn’t only about fixing unexpected crashes. It also includes writing code that gracefully handles errors when they occur. This is called exception handling.
The try / except / else / finally Structure
Python gives you four blocks for handling exceptions:
try:
# Code that might raise an exception
result = 10 / int(input("Enter a number: "))
except ZeroDivisionError:
# Runs if a ZeroDivisionError occurs
print("Error: You cannot divide by zero.")
except ValueError:
# Runs if the user enters non-numeric input
print("Error: Please enter a valid number.")
else:
# Runs ONLY if no exception occurred
print(f"Result: {result}")
finally:
# ALWAYS runs, exception or not
print("Calculation complete.")
| Block | When It Runs |
|---|---|
try | Always — this is where your main logic goes |
except | Only when the specified exception is raised |
else | Only when NO exception occurred |
finally | Always — even if an exception occurred |
Catching Multiple Exceptions
try:
data = int(input("Enter a number: "))
result = 100 / data
except (ValueError, ZeroDivisionError) as e:
print(f"Input error: {e}")
The Golden Rules of Exception Handling
Rule 1: Never use a bare except:
# BAD — catches everything including system exits and keyboard interrupts
try:
do_something()
except:
pass
# GOOD — catch only what you expect
try:
do_something()
except ValueError as e:
print(f"ValueError caught: {e}")
Rule 2: Don’t swallow exceptions silently
An empty except block that does nothing (pass) is one of the most dangerous things you can write. It hides bugs instead of exposing them.
Rule 3: Use logging.exception() inside except blocks
import logging
try:
result = risky_operation()
except Exception as e:
logging.exception("Unexpected error in risky_operation")
# This logs the full traceback automatically
Rule 4: Re-raise when appropriate
Sometimes you want to log an exception but still let it propagate up:
try:
connect_to_database()
except ConnectionError as e:
logging.error("Database connection failed")
raise # Re-raise the original exception
Reading Tracebacks Inside except Blocks
Python’s traceback module lets you capture and display tracebacks programmatically:
import traceback
try:
broken_function()
except Exception:
traceback.print_exc() # Prints the full traceback to stderr
Or capture it as a string:
import traceback
try:
broken_function()
except Exception:
error_details = traceback.format_exc()
print(f"Something went wrong:\n{error_details}")
For the complete Python exception handling reference, see the official Python Errors and Exceptions documentation.
Using Python’s logging Module for Debugging
Once you move beyond simple scripts, print() debugging starts showing its limitations. You can’t easily control the verbosity, it clutters your output, and you have to manually delete every debug statement before deploying.
The logging module solves all of this. It’s Python’s built-in, professional-grade logging system — and it comes with your Python installation for free.
Why logging Beats print() for Real Projects
| Feature | print() | logging |
|---|---|---|
| Severity levels | ❌ None | ✅ DEBUG, INFO, WARNING, ERROR, CRITICAL |
| Write to files | ❌ Manual redirect | ✅ Built-in |
| Turn on/off easily | ❌ Delete statements | ✅ Change log level |
| Include timestamps | ❌ Manual | ✅ Automatic |
| Filter by module | ❌ No | ✅ Yes |
| Production-safe | ❌ No | ✅ Yes |
The Five Logging Levels
Python’s logging module has five standard severity levels:
| Level | Numeric Value | When to Use |
|---|---|---|
DEBUG | 10 | Detailed diagnostic info — variable values, flow tracing |
INFO | 20 | Confirmation that things are working as expected |
WARNING | 30 | Something unexpected happened, but the program continues |
ERROR | 40 | A serious problem — something failed |
CRITICAL | 50 | A very serious error — program may not be able to continue |
Basic Logging Setup
import logging
# Configure logging — do this once at the top of your script
logging.basicConfig(
level=logging.DEBUG, # Show all levels
format="%(asctime)s - %(levelname)s - %(message)s", # Format
filename="debug.log", # Write to file
filemode="w" # Overwrite each run
)
# Now log at different levels
logging.debug("Starting calculation")
logging.info("Processing 50 records")
logging.warning("File not found, using defaults")
logging.error("Failed to connect to database")
logging.critical("System out of memory!")
Output in debug.log:
2026-04-15 10:23:01,234 - DEBUG - Starting calculation
2026-04-15 10:23:01,235 - INFO - Processing 50 records
2026-04-15 10:23:01,236 - WARNING - File not found, using defaults
2026-04-15 10:23:01,237 - ERROR - Failed to connect to database
2026-04-15 10:23:01,238 - CRITICAL - System out of memory!
Logging During Debugging — A Practical Example
import logging
logging.basicConfig(level=logging.DEBUG,
format="%(levelname)s: %(message)s")
def divide(a, b):
logging.debug(f"divide() called with a={a}, b={b}")
try:
result = a / b
logging.info(f"Division successful: {a} / {b} = {result}")
return result
except ZeroDivisionError as e:
logging.error(f"Cannot divide {a} by zero: {e}")
return None
divide(10, 2) # Works fine
divide(10, 0) # Triggers the error path
Output:
DEBUG: divide() called with a=10, b=2
INFO: Division successful: 10 / 2 = 5.0
DEBUG: divide() called with a=10, b=0
ERROR: Cannot divide 10 by zero: division by zero
Switching Between Environments
One of logging’s greatest advantages: you can show verbose debug output during development and silently suppress it in production simply by changing the log level:
# Development
logging.basicConfig(level=logging.DEBUG)
# Production — only show warnings and above
logging.basicConfig(level=logging.WARNING)
No code changes needed. No hunting down print() statements.
⚙️ Real-World Use: Logging becomes critical in production automation. If you’re building scripts like sending emails automatically with Python, logging every success and failure is essential for diagnosing issues when things go wrong in production.
For complete logging documentation, see the Python Logging HOWTO guide.
Debugging Loops and Conditions
Loops and conditional statements are the most common source of logical errors in beginner Python code. Here’s how to debug them effectively.
Common Loop Bugs
1. Off-by-one errors
# Bug: misses the last element
numbers = [1, 2, 3, 4, 5]
for i in range(len(numbers) - 1): # Should be range(len(numbers))
print(numbers[i])
# Better: use the list directly
for num in numbers:
print(num)
2. Infinite loops
# Bug: i never changes, loop runs forever
i = 0
while i < 10:
print(i)
# Missing: i += 1
# Fix:
i = 0
while i < 10:
print(i)
i += 1 # Update the loop variable!
If you accidentally run an infinite loop, press Ctrl+C in your terminal to interrupt it.
3. Modifying a list while iterating over it
# Bug: skips elements
my_list = [1, 2, 3, 4, 5]
for item in my_list:
if item % 2 == 0:
my_list.remove(item) # Never modify a list mid-loop
# Fix: iterate over a copy
for item in my_list[:]:
if item % 2 == 0:
my_list.remove(item)
Common Conditional Logic Bugs
1. Assignment instead of comparison
# Bug: assigns 5 to x instead of comparing
if x = 5: # SyntaxError — Python will catch this
print("Five")
# Correct:
if x == 5:
print("Five")
2. Floating-point comparison
# Bug: floating-point precision issue
result = 0.1 + 0.2
if result == 0.3: # This is False! (result is 0.30000000000000004)
print("Equal")
# Fix: use a tolerance
if abs(result - 0.3) < 1e-9:
print("Equal")
3. Wrong and/or logic
# Bug: condition is always True when x > 0 OR x < 10
# (which is always true for any number)
if x > 0 or x < 10: # Should probably be 'and'
process(x)
# Fix: use 'and' to enforce both conditions
if x > 0 and x < 10: # Or: if 0 < x < 10
process(x)
Using assert for Sanity Checks
The assert statement is a quick way to verify that your assumptions are correct:
def calculate_percentage(part, total):
assert total > 0, f"Total must be positive, got {total}"
assert 0 <= part <= total, f"Part ({part}) must be between 0 and total ({total})"
return (part / total) * 100
calculate_percentage(30, 100) # Works fine
calculate_percentage(30, 0) # AssertionError: Total must be positive, got 0
assert statements can be disabled in production by running Python with the -O flag (optimize), so they’re perfect for development-only checks.
Common Mistakes Beginners Make While Debugging
Knowing what not to do is just as important as knowing the right techniques. Here are the most common debugging mistakes — and how to avoid them.
1. Fixing Symptoms Instead of Root Causes
If your program crashes on line 47, the real bug might be on line 12 where you set up the data incorrectly. Always trace back to the source.
2. Changing Multiple Things at Once
When you change three things and the bug disappears, you don’t know which change fixed it. Worse — if the bug doesn’t disappear, you don’t know which change made it worse. Make one change at a time. Test. Repeat.
3. Not Reading the Full Traceback
The most common beginner mistake: seeing a red error message and immediately assuming you know what it means without reading it. Read every word of the traceback. Python put it there to help you.
4. Forgetting to Save the File
This sounds obvious, but it happens constantly. You fix a bug, run the script, it still fails — because you forgot to save. Most IDEs show an indicator when a file has unsaved changes.
5. Leaving print() Statements in Production Code
Debug prints that you forget to remove can leak sensitive information or clutter your application’s output. Always clean up when done — or better yet, use the logging module instead.
6. Not Testing Edge Cases
Most bugs hide in edge cases — what happens when the input is empty, zero, None, a negative number, or a very large number? Always test these:
# Test your function with edge cases
print(find_average([])) # Empty list
print(find_average([0])) # Single zero
print(find_average([-5, 5])) # Negatives
print(find_average([10**9])) # Very large number
7. Trusting Your Assumptions
“The data must be an integer, so I don’t need to check.” Famous last words. Never assume — verify with type checks, assertions, or print statements.
8. Copy-Pasting Code Without Understanding It
If you copy a block of code and paste it elsewhere, you copy the bug too. Always understand what every line does.
9. Not Using Version Control
When your “fix” breaks three new things, you want to be able to undo it. Use Git to commit your working code before making changes:
git commit -m "Working version before debugging attempt"
📊 Data-specific bugs: When debugging pandas DataFrames, beginners often inspect data incorrectly. Our Pandas tutorial for beginners (2026) shows how to validate and inspect data step-by-step at each transformation.
Python Debugging Best Practices (2026)
These are the habits that separate developers who debug efficiently from those who waste hours:
✅ 1. Reproduce the Bug First
You cannot fix a bug you cannot reliably reproduce. Find the exact input or conditions that cause the failure — and make sure it fails consistently before trying to fix it.
✅ 2. Minimize the Failing Code
If you have a 500-line script with a bug, try to reproduce the bug in 20 lines. This technique — called creating a minimal reproducible example — often reveals the bug just by doing it.
✅ 3. Understand the Error Message Before Googling
Spend 60 seconds genuinely trying to understand the error message before searching online. You’ll find the answer yourself more often than you expect — and you’ll learn faster.
✅ 4. Use Descriptive Variable Names
Code with clear names is dramatically easier to debug than code with single-letter variables:
# Hard to debug
t = s * r
# Easy to debug
total_price = subtotal * tax_rate
✅ 5. Write Small, Single-Purpose Functions
A function that does one thing and does it well is easy to test and easy to debug. A 200-line function that does everything is a nightmare.
✅ 6. Use Type Hints (Python 3.10+)
def calculate_discount(price: float, discount_percent: float) -> float:
return price * (1 - discount_percent / 100)
Type hints help IDEs and static analysis tools catch type-related bugs before you run your code.
✅ 7. Use Static Analysis Tools
In 2026, two tools are essential for catching bugs early:
- Ruff — an extremely fast Python linter that catches syntax errors, undefined variables, unused imports, and hundreds of other issues as you type
- Mypy — a type checker that uses your type hints to catch type errors before runtime
Install both:
pip install ruff mypy
Run them on your code:
ruff check your_script.py
mypy your_script.py
✅ 8. Rubber Duck Debugging
When you’re completely stuck, explain your code out loud — to a colleague, to a rubber duck on your desk, or just to yourself. The act of verbalizing the problem forces your brain to process it differently, and you’ll often spot the bug mid-sentence.
✅ 9. Take Breaks
If you’ve been staring at the same bug for an hour, walk away for 15 minutes. Fresh eyes spot things that tired eyes miss. This is not wasted time — it’s part of the process.
✅ 10. Commit Before You Debug
Before making any changes, commit your current state to Git:
git add .
git commit -m "Pre-debug snapshot"
This gives you a safety net if your fix makes things worse.
Real-World Debugging Scenarios
Let’s apply everything we’ve learned to realistic debugging situations.
Scenario 1: Debugging a File Read Error
The Problem:
with open("data.txt") as f:
content = f.read()
The Error:
FileNotFoundError: [Errno 2] No such file or directory: 'data.txt'
How to Debug:
Step 1 — Read the traceback. FileNotFoundError means the file doesn’t exist at the path Python is looking for.
Step 2 — Add a debug print to check where Python is looking:
import os
print(f"[DEBUG] Current working directory: {os.getcwd()}")
print(f"[DEBUG] Files in directory: {os.listdir('.')}")
Step 3 — The fix might be:
- Providing an absolute path:
open("/home/user/projects/data.txt") - Checking whether the file actually exists:
if os.path.exists("data.txt"): - Creating the file if it’s missing
📂 For a complete guide to file handling in Python including common errors and their fixes, see how to read and write text files in Python.
Scenario 2: Debugging a Flask Route That Returns 500
The Problem: A Flask route is returning an Internal Server Error.
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/calculate", methods=["POST"])
def calculate():
data = request.get_json()
result = data["value"] * 2 # Possible bug here
return jsonify({"result": result})
How to Debug:
Step 1 — Enable Flask’s debug mode:
app.run(debug=True) # Never use in production!
Step 2 — The browser now shows the full traceback. Maybe the client isn’t sending the "value" key, causing a KeyError.
Step 3 — Add defensive handling:
@app.route("/calculate", methods=["POST"])
def calculate():
data = request.get_json()
if not data or "value" not in data:
return jsonify({"error": "Missing 'value' in request body"}), 400
result = data["value"] * 2
return jsonify({"result": result})
Step 4 — Add logging to track all incoming requests:
import logging
logging.basicConfig(level=logging.DEBUG)
@app.route("/calculate", methods=["POST"])
def calculate():
logging.debug(f"Request received: {request.get_json()}")
# ... rest of the code
🌐 Building your first Flask application? Our complete Flask beginner’s guide (2026) walks you through routing, templates, and debugging common Flask errors.
Scenario 3: A Logic Error in a Game’s Score Counter
The Problem: A number guessing game gives wrong feedback.
import random
secret = random.randint(1, 100)
attempts = 0
while True:
guess = int(input("Guess the number (1-100): "))
attempts = attempts + 1
if guess < secret:
print("Too low!")
if guess > secret:
print("Too high!")
if guess == secret:
print(f"Correct! You got it in {attempts} guesses.")
break
The Bug: When guess == secret, the program prints “Correct!” but also prints “Too low!” or “Too high!” from the previous conditions… wait, no it doesn’t. But notice the bug: if the user guesses correctly on the first try with guess=50 and secret=50, everything works. But what if you want to add more logic later — this uses if / if / if instead of if / elif / elif, meaning all three conditions are evaluated every time. This is a subtle logical design flaw.
The Fix:
if guess < secret:
print("Too low!")
elif guess > secret:
print("Too high!")
else:
print(f"Correct! You got it in {attempts} guesses.")
break
Using elif and else ensures only one branch executes — cleaner logic, fewer bugs.
Debugging technique used: Print the value of guess and secret on each iteration to trace what the condition checks are seeing.
Scenario 4: A Pandas Filter Returns Unexpected Rows
The Problem:
import pandas as pd
df = pd.read_csv("sales.csv")
high_sales = df[df["revenue"] > 10000]
print(f"High sales count: {len(high_sales)}") # Prints 0, expected ~50
How to Debug:
Step 1 — Inspect the data types:
print(df.dtypes) # Is 'revenue' a string instead of float?
print(df["revenue"].head()) # Look at the actual values
Step 2 — The fix: the revenue column might be stored as strings (e.g., "$10,500"):
# Clean the column first
df["revenue"] = df["revenue"].str.replace("$", "").str.replace(",", "").astype(float)
high_sales = df[df["revenue"] > 10000]
Lesson: Always inspect data types before filtering. df.dtypes and df.head() are your best debugging friends in pandas.
Frequently Asked Questions (FAQs)
Q1: What is the easiest way to debug Python code for beginners?
Start with print() statements to track variable values at different points in your code. Add labels like [DEBUG] to make them easy to spot. Once you’re comfortable, move to breakpoint() or your IDE’s built-in debugger for more control without modifying your code.
Q2: What is pdb in Python and how do I use it?
pdb is Python’s built-in interactive debugger. Add breakpoint() to your code where you want to pause (Python 3.7+), then run your script normally. When Python reaches that line, it pauses and gives you an interactive prompt where you can inspect variables, step through code line by line, and test fixes in real time.
Q3: How do I read a Python traceback?
Start from the bottom of the traceback — the last line tells you the error type (e.g., TypeError) and the specific message. The lines above show the chain of function calls that led to the error. Work from the last line upward to trace the source of the problem.
Q4: What is the difference between a syntax error and a runtime error?
A syntax error prevents Python from even reading your code — it’s like a grammar mistake in a sentence. A runtime error occurs while the code is running, under specific conditions. Syntax errors always appear before the program starts; runtime errors appear during execution.
Q5: Should I use print() or logging for debugging?
Use print() for quick, throwaway checks on small scripts. Use logging for any project you plan to maintain, share, or deploy. Logging lets you categorize messages by severity, write to files, and filter output — all without deleting a single line of code.
Q6: How do I debug an infinite loop in Python?
Press Ctrl+C to stop it immediately. Then add a print statement inside the loop to see the values of your loop variable on each iteration. Check that your loop condition will eventually become False — the most common cause is forgetting to update the loop variable.
Q7: What are the best Python debugging tools in 2026?
For terminal-based debugging: pdb (via breakpoint()). For visual debugging: VS Code with the Python Debugger extension, or PyCharm. For catching bugs before running: Ruff (linter) and Mypy (type checker). For production monitoring: Python’s built-in logging module.
Q8: What is a logical error and how do I fix it?
A logical error is when your code runs without crashing but produces incorrect results. There’s no error message — the program just gives the wrong answer. Fix them by: (1) adding print() or logging statements to inspect intermediate values, (2) writing test cases with known inputs and expected outputs, and (3) reviewing your algorithms and formulas carefully.
Conclusion
Let’s take a moment to appreciate how far you’ve come in this guide.
You started knowing Python throws red error messages and you weren’t sure what to do with them. Now you understand:
- The three types of Python errors — syntax, runtime, and logical — and how to approach each one
- How to read a traceback from bottom to top and extract exactly what went wrong
- How to use
print()debugging effectively with labels and f-strings - How to invoke
pdbwithbreakpoint()and navigate your code interactively - How to use visual debuggers in VS Code and PyCharm with breakpoints, step controls, and variable inspection panels
- How to write proper exception handling with
try/except/else/finally - How to use the
loggingmodule as a professional, production-safe replacement forprint() - The common mistakes beginners make and how to avoid them
- Best practices like minimizing failing code, using type hints, and committing before debugging
Debugging is a skill, and like all skills, it improves with practice. Every bug you fix teaches you something new about Python, about your own assumptions, and about the art of writing clear, maintainable code.
The best developers aren’t the ones who write bug-free code on the first try — they’re the ones who find and fix bugs faster than everyone else.
🚀 Ready to put your new skills to the test? Head over to the PyCodeRoom AI Task Generator for beginner-friendly Python challenges designed to sharpen your problem-solving and debugging abilities. Each challenge is a chance to write code, introduce bugs, and practice everything you’ve learned here.
