How to debug Python code

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 pdb debugger
  • Visual debugging in VS Code and PyCharm
  • Setting breakpoints and stepping through code
  • Handling exceptions and tracebacks properly
  • Using the logging module 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:

  1. Reproducing the error reliably
  2. Locating where in the code the problem occurs
  3. Understanding why the problem happens
  4. Fixing the root cause (not just the symptom)
  5. 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, class statements
  • 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 TypeCauseExample
NameErrorUsing a variable that doesn’t existprint(total) before defining total
TypeErrorWrong data type for an operation"age" + 25
IndexErrorAccessing a list index that doesn’t existmy_list[10] on a 5-item list
ZeroDivisionErrorDividing by zero10 / 0
KeyErrorAccessing a dictionary key that doesn’t existmy_dict["missing_key"]
AttributeErrorCalling a method that doesn’t exist on an object"hello".uppercase()
FileNotFoundErrorTrying to open a file that doesn’t existopen("data.txt")
ValueErrorCorrect type but invalid valueint("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:

PartWhat 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_cartWhich 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:

  1. The type of error (KeyError)
  2. 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:

CommandShort FormWhat It Does
nextnExecute the next line (don’t enter functions)
stepsStep into the next function call
continuecRun until the next breakpoint
listlShow the code around the current line
print variablep variablePrint a variable’s value
break line_numberb 10Set a breakpoint at line 10
quitqExit the debugger
wherewShow the current call stack
up / downNavigate 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:

ButtonShortcutAction
ContinueF5Run to the next breakpoint
Step OverF10Execute current line, skip into functions
Step IntoF11Enter the function being called
Step OutShift+F11Finish current function, return to caller
StopShift+F5Stop 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.")
BlockWhen It Runs
tryAlways — this is where your main logic goes
exceptOnly when the specified exception is raised
elseOnly when NO exception occurred
finallyAlways — 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

Featureprint()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:

LevelNumeric ValueWhen to Use
DEBUG10Detailed diagnostic info — variable values, flow tracing
INFO20Confirmation that things are working as expected
WARNING30Something unexpected happened, but the program continues
ERROR40A serious problem — something failed
CRITICAL50A 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 pdb with breakpoint() 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 logging module as a professional, production-safe replacement for print()
  • 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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *