Coding

Coding

A Beginner's Guide to Python Debugging: Why Your Code Is Breaking and What It Means

November 16, 2025

9

min read

You're learning Python. You write what seems like perfectly logical code. You hit run.

Traceback (most recent call last):
  File "main.py", line 12, in <module>
    result = calculate_total(items)
  File "main.py", line 8, in calculate_total
    total += item['price']

Your heart sinks. What does this mean? Where did you go wrong? Why is Python so cryptic?

Python errors aren't cryptic. They're actually incredibly specific. You just haven't learned to read them yet.

This guide will teach you how to debug Python code the way someone actually learning the language needs to learn it. Not shortcuts. Not magic tools. The fundamental skills that make you a better programmer.

No prior debugging experience required. Just a willingness to understand why things break.

Why Debugging Matters More Than You Think

When you start learning Python, most tutorials focus on syntax. Here's how to write a loop. Here's how to define a function. Here's how to use a dictionary.

What they don't teach you: how to figure out why your code isn't working.

Debugging isn't a separate skill from programming. It's fundamental to programming. You will spend more time debugging than writing new code. This isn't a flaw. It's the nature of building software.

Professional developers debug constantly. The difference between a beginner and an experienced programmer isn't that experienced programmers write perfect code. It's that they debug faster because they recognize patterns.

Learning to debug well makes everything else easier. You become self-sufficient. You stop getting stuck for hours on simple mistakes. You start actually understanding what your code is doing.

The Anatomy of a Python Error Message

Let's start with the most important skill: reading error messages.

Python errors follow a consistent structure. Once you understand the pattern, they become helpful instead of intimidating.

The Traceback: Your Error Trail

When Python encounters an error, it gives you a traceback. This is a trail showing exactly how your code arrived at the error.

Traceback (most recent call last):
  File "main.py", line 15, in <module>
    process_data(user_input)
  File "main.py", line 10, in process_data
    result = calculate_score(data)
  File "main.py", line 5, in calculate_score
    return sum(scores) / len(scores)
ZeroDivisionError: division by zero

Read the traceback from bottom to top. That's where the actual error is.

Bottom line (the error itself):

This tells you:

  • Error type: ZeroDivisionError

  • What happened: "division by zero"

Line above it (where the error occurred):


This tells you:

  • File: main.py

  • Line number: 5

  • Function: calculate_score

  • The actual code that failed: return sum(scores) / len(scores)

Lines above that (how you got there): These show the sequence of function calls that led to the error. In this case:

  1. Started in main code (line 15)

  2. Called process_data()

  3. Which called calculate_score()

  4. Which tried to divide by zero

This is your debugging roadmap. It tells you exactly where the error is and how you got there.

Common Python Error Types and What They Actually Mean

Let's decode the errors you'll encounter most frequently.

SyntaxError: Python Doesn't Understand Your Code

SyntaxError: invalid syntax

What it means: You wrote something that isn't valid Python. This is like a grammatical error in English.

Common causes:

  • Missing colon after if, for, while, def, or class

  • Mismatched parentheses, brackets, or quotes

  • Using a reserved word incorrectly

  • Incorrect indentation

Example:

if x > 5
    print("Greater than 5")

Error location:


The fix: Missing colon after the if statement.

if x > 5:
    print("Greater than 5")

Pro tip: The caret (^) points to where Python got confused, which is often right after the actual mistake.

IndentationError: Your Spacing Is Wrong

IndentationError: expected an indented block

What it means: Python uses indentation to define code blocks. You either didn't indent when you should have, or your indentation is inconsistent.

Python requires consistent indentation. You must use either spaces or tabs, never both. Standard convention is 4 spaces.

Example:

def greet(name):
print(f"Hello, {name}")

Error:

The fix: Indent the function body.

def greet(name):
    print(f"Hello, {name}")

Common variation:

IndentationError: unindent does not match any outer indentation level

This happens when you mix tabs and spaces, or your indentation is inconsistent.

NameError: Python Doesn't Recognize This Name

NameError: name 'total' is not defined

What it means: You're trying to use a variable that doesn't exist (yet).

Common causes:

  • Typo in variable name

  • Using a variable before defining it

  • Variable only exists inside a function (scope issue)

  • Forgot to import a module

Example:

print(total)

Error:

The fix: Define the variable first.

total = 0
print(total)

Scope-related example:

def calculate():
    result = 100
    
print(result)  # NameError: result only exists inside calculate()

TypeError: You're Using the Wrong Type

TypeError: unsupported operand type(s) for +: 'int' and 'str'

What it means: You're trying to do something with incompatible data types.

Example:

age = 25
message = "You are " + age + " years old"

Error:

What happened: You can't add a string and an integer directly.

The fix: Convert the integer to a string.

age = 25
message = "You are " + str(age) + " years old"
# or use f-strings
message = f"You are {age} years old"

Another common TypeError:

TypeError: 'str' object is not callable

Example:

name = "Alice"
name = str.upper  # Accidentally overwrote the variable
print(name("Bob"))  # Trying to call name as a function

IndexError: You're Accessing Something That Doesn't Exist

IndexError: list index out of range

What it means: You're trying to access an element at an index that doesn't exist in the list.

Example:

numbers = [1, 2, 3]
print(numbers[3])  # Lists are 0-indexed, so valid indices are 0, 1, 2

Error:

Common cause: Off-by-one errors. Python lists start at index 0, not 1.

The fix: Use valid indices.

numbers = [1, 2, 3]
print(numbers[2])  # Gets the third element (index 2)
# or
print(numbers[-1])  # Gets the last element

KeyError: Dictionary Key Doesn't Exist

KeyError: 'email'

What it means: You're trying to access a dictionary key that doesn't exist.

Example:

user = {"name": "Alice", "age": 30}
print(user["email"])

Error:

The fix: Check if the key exists first, or use .get().

# Option 1: Check if key exists
if "email" in user:
    print(user["email"])
else:
    print("No email found")

# Option 2: Use .get() with a default value
email = user.get("email", "No email provided")
print(email)

AttributeError: This Object Doesn't Have That Attribute

AttributeError: 'str' object has no attribute 'append'

What it means: You're trying to use a method or attribute that doesn't exist on this type of object.

Example:

name = "Alice"
name.append("!")  # Strings don't have an append() method

Error:

What happened: You're treating a string like a list. Strings and lists have different methods.

The fix: Use the correct method for the type.

name = "Alice"
name = name + "!"  # Concatenation for strings
# or
name_list = ["Alice"]
name_list.append("!")  # append() works on lists

ValueError: Right Type, Wrong Value

ValueError: invalid literal for int() with base 10: 'abc'

What it means: You're passing a value that's the right type but can't be processed.

Example:

number = int("abc")  # Can't convert "abc" to an integer

Error:

The fix: Validate input before converting.

user_input = "abc"
if user_input.isdigit():
    number = int(user_input)
else:
    print("Please enter a valid number")

Another common ValueError:

numbers = [1, 2, 3]
numbers.remove(4)  # 4 isn't in the list

Error:

The Debugging Mindset: How to Think About Errors

Errors aren't failures. They're information. When your code breaks, Python is telling you exactly what went wrong. Learning to listen is the skill.

The Scientific Method for Debugging

1. Observe the error Read the entire error message. What type of error? What line? What's the error message?

2. Form a hypothesis Based on the error, what do you think went wrong?

3. Test your hypothesis Add print statements, check variable values, trace the logic.

4. Adjust and repeat If your hypothesis was wrong, form a new one based on what you learned.

The Three Questions to Ask Every Time

Question 1: What did I expect to happen? Write down what you thought the code would do.

Question 2: What actually happened? Look at the error or unexpected output.

Question 3: What's the difference? The gap between expectation and reality is where the bug lives.

Example:

Code:

def calculate_average(numbers):
    total = sum(numbers)
    average = total / len(numbers)
    return average

scores = []
result = calculate_average(scores)
print(result)

Expected: The code would calculate the average of the scores.

Actual:

Difference: I expected scores to have values, but it's empty. Dividing by zero (the length of an empty list) causes the error.

Fix: Handle the edge case.

def calculate_average(numbers):
    if len(numbers) == 0:
        return 0  # or return None, or raise a custom error
    total = sum(numbers)
    average = total / len(numbers)
    return average

Debugging Techniques That Actually Work

Theory is useful. Practice is better. Here are the techniques you'll use constantly.

Technique 1: Print Debugging (Your Best Friend)

The simplest and most effective debugging technique: print what's happening.

Example problem:

def process_items(items):
    total = 0
    for item in items:
        total += item['price']
    return total

cart = [
    {'name': 'Book', 'price': 15},
    {'name': 'Pen', 'price': 2},
    'Invalid item'  # This will cause an error
]

result = process_items(cart)

Error:

Use print statements to investigate:

def process_items(items):
    print(f"Processing {len(items)} items")  # How many items?
    total = 0
    for item in items:
        print(f"Current item: {item}")  # What is each item?
        print(f"Type of item: {type(item)}")  # What type is it?
        total += item['price']
    return total

Output:


Now it's obvious: The third item is a string, not a dictionary. You can't access ['price'] on a string.

Pro tip: Use descriptive print statements with labels so you know what you're looking at.

Technique 2: Comment Out Code to Isolate the Problem

When you're not sure which part is causing the error, comment out sections to narrow it down.

Example:

# Lots of code here that might be causing the error
result1 = complex_calculation1()
result2 = complex_calculation2()
result3 = complex_calculation3()
final = combine_results(result1, result2, result3)

Strategy: Comment out parts to isolate.

result1 = complex_calculation1()
print("Result 1 completed")  # Check if this works

# result2 = complex_calculation2()
# result3 = complex_calculation3()
# final = combine_results(result1, result2, result3)

If result1 works, uncomment result2 and test. Continue until you find which function is failing.

Technique 3: Check Your Assumptions with Type and Value Checks

Often bugs happen because a variable isn't what you think it is.

Helpful checks:

# Check what type something is
print(type(variable))

# Check what's in a variable
print(variable)
print(repr(variable))  # Shows the exact representation, including hidden characters

# Check if it's empty
print(len(variable))  # For lists, strings, dictionaries

# Check if it exists
print(variable is None)

# For dictionaries, check keys
print(my_dict.keys())

# For objects, check attributes
print(dir(my_object))  # Shows all available methods and attributes

Technique 4: Test Edge Cases

Bugs often hide in edge cases. Test with:

Empty inputs:

process_data([])  # Empty list
process_data("")  # Empty string
process_data({})  # Empty dictionary

Single-item inputs:

process_data([1])  # Single item

Unexpected types:

process_data(None)
process_data(0)
process_data(-1)

Boundary values:

# If your code handles 1-100, test:
process_data(0)   # Below range
process_data(1)   # Minimum
process_data(100) # Maximum
process_data(101) # Above range

Technique 5: Read the Error Message Carefully (Again)

You'd be surprised how often the error message tells you exactly what's wrong, but you missed it the first time.

Error:

FileNotFoundError: [Errno 2]

What it tells you:

  • Error type: FileNotFoundError

  • The file: 'data.txt'

  • The problem: The file doesn't exist

Common reasons:

  • File is in a different directory

  • Filename typo

  • File hasn't been created yet

  • Wrong file path

The fix:

import os

# Check if file exists before trying to open it
if os.path.exists('data.txt'):
    with open('data.txt', 'r') as file:
        content = file.read()
else:
    print("File not found. Creating new file...")
    with open('data.txt', 'w') as file:
        file.write("")

Technique 6: Simplify Until It Works, Then Add Back Complexity

If you have complex code that's breaking, simplify it to the bare minimum that works, then add complexity back piece by piece.

Complex broken code:

def process_user_data(users, filters, transformations, output_format):
    filtered = apply_filters(users, filters)
    transformed = apply_transformations(filtered, transformations)
    formatted = format_output(transformed, output_format)
    return formatted

Simplify to minimum:

def process_user_data(users):
    return users  # Does this work?

Add back one piece at a time:

def process_user_data(users, filters):
    filtered = apply_filters(users, filters)
    return filtered  # Does filtering work?

Then add transformations, then formatting. You'll find exactly where it breaks.

Common Beginner Mistakes and How to Spot Them

These are the errors that trip up everyone when learning Python.

Mistake 1: Confusing = (Assignment) with == (Comparison)

Wrong:

if x = 5:  # SyntaxError: invalid syntax
    print("X is 5")

Right:

if x == 5:  # Use == for comparison
    print("X is 5")

How to remember:

  • = assigns a value ("x becomes 5")

  • == checks equality ("is x equal to 5?")

Mistake 2: Forgetting to Return a Value from a Function

Wrong:

def calculate_total(price, quantity):
    total = price * quantity
    # Forgot to return!

result = calculate_total(10, 5)
print(result)  # Prints: None

Right:

def calculate_total(price, quantity):
    total = price * quantity
    return total  # Return the value

How to spot: If your function result is None when it shouldn't be, you probably forgot return.

Mistake 3: Modifying a List While Iterating Over It

Wrong:

numbers = [1, 2, 3, 4, 5]
for num in numbers:
    if num % 2 == 0:
        numbers.remove(num)  # Modifying while iterating causes issues

print(numbers)  # Unexpected result: [1, 3, 5] but might skip elements

Right:

numbers = [1, 2, 3, 4, 5]
numbers = [num for num in numbers if num % 2 != 0]  # Create new list
print(numbers)  # [1, 3, 5]

Mistake 4: Using Mutable Default Arguments

Wrong:

def add_item(item, items=[]):  # Empty list created once, not each time
    items.append(item)
    return items

print(add_item("apple"))   # ['apple']
print(add_item("banana"))  # ['apple', 'banana'] - Wait, what?

What happened: The default [] is created once when the function is defined, not each time it's called.

Right:

def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item("apple"))   # ['apple']
print(add_item("banana"))  # ['banana'] - Correct!

Mistake 5: Not Understanding Indentation Scope

Wrong:

for i in range(3):
    print(f"Loop iteration {i}")
    total = 0  # This gets reset every iteration!
    total += i

print(total)  # Only has the last value

Right:

total = 0  # Initialize outside the loop
for i in range(3):
    print(f"Loop iteration {i}")
    total += i  # Accumulates across iterations

print(total)  # 3 (0 + 1 + 2)

Mistake 6: Integer Division Surprise (Python 2 vs 3)

Python 2 behavior (if you're using old code):

result = 5 / 2  # Returns 2 (integer division)

Python 3 behavior:

result = 5 / 2   # Returns 2.5 (float division)
result = 5 // 2  # Returns 2 (integer division)

If you want integer division in Python 3, use //.

Building a Debugging Workflow

Here's a step-by-step process to follow every time you encounter an error.

Step 1: Read the Error Message

Don't panic. Read carefully.

  • What type of error?

  • What line number?

  • What's the error message?

Step 2: Go to the Line Number

Navigate to the exact line where the error occurred.

Step 3: Check Variable Values at That Line

Add print statements right before the error to see what values exist:

print(f"variable1: {variable1}")
print(f"variable2: {variable2}")
# The line that's causing the error

Step 4: Trace Backwards

If the error is because a variable has the wrong value, trace back to where that variable gets its value.

Step 5: Form a Hypothesis

Based on what you've learned, what do you think is wrong?

Step 6: Test Your Fix

Make a small change and run the code again. Did it fix the error? Did it create a new error?

Step 7: Repeat Until Solved

Debugging is iterative. Each attempt teaches you something.

Using Python's Built-in Debugging Tools

Beyond print statements, Python has built-in tools for debugging.

The assert Statement

Use assertions to verify assumptions during development.

def calculate_average(numbers):
    assert len(numbers) > 0, "List cannot be empty"
    return sum(numbers) / len(numbers)

calculate_average([])  # AssertionError: List cannot be empty

Assertions help catch bugs early. They make your assumptions explicit.

The try-except Block

Handle errors gracefully instead of crashing.

try:
    user_input = input("Enter a number: ")
    number = int(user_input)
    result = 100 / number
    print(f"Result: {result}")
except ValueError:
    print("That's not a valid number")
except ZeroDivisionError:
    print("Cannot divide by zero")
except Exception as e:
    print(f"An unexpected error occurred: {e}")

Use try-except when:

  • You're handling user input

  • Working with external resources (files, network)

  • You expect an error might occur and want to handle it

Don't use try-except to hide bugs. Fix the actual problem.

The pdb Debugger (Interactive Debugging)

Python's built-in debugger lets you pause code and inspect it interactively.

import pdb

def calculate_total(items):
    total = 0
    for item in items:
        pdb.set_trace()  # Execution pauses here
        total += item['price']
    return total

When you hit pdb.set_trace(), you get an interactive prompt:

  • n (next): Execute the next line

  • c (continue): Continue until next breakpoint

  • p variable_name: Print a variable's value

  • l (list): Show surrounding code

  • q (quit): Exit the debugger

Useful for: Complex bugs where you need to inspect variables at specific points.

The dir() and help() Functions

dir(object): Shows all attributes and methods available on an object.

my_list = [1, 2, 3]
print(dir(my_list))  # Shows: append, extend, pop, etc.

help(object): Shows documentation for an object or method.

help(list.append)  # Shows documentation for the append method

Use these to discover what methods are available when you're not sure.

Real-World Debugging Examples

Let's debug actual code scenarios you'll encounter.

Example 1: The Mysterious Empty Result

Problem:

def filter_adults(people):
    adults = []
    for person in people:
        if person['age'] >= 18:
            adults.append(person)

people = [
    {'name': 'Alice', 'age': 25},
    {'name': 'Bob', 'age': 17},
    {'name': 'Carol', 'age': 30}
]

result = filter_adults(people)
print(result)  # Prints: None - Why?

Debugging process:

1. Check the return value:

def filter_adults(people):
    adults = []
    for person in people:
        if person['age'] >= 18:
            adults.append(person)
    print(f"Adults list: {adults}")  # Add this
    # Missing return statement!

Output:

Adults list: [{'name': 'Alice', 'age': 25}, {'name': 'Carol', 'age': 30}]

2. Identify the problem: The list is built correctly but not returned.

3. Fix:

def filter_adults(people):
    adults = []
    for person in people:
        if person['age'] >= 18:
            adults.append(person)
    return adults  # Add return statement

Example 2: The Off-By-One Error

Problem:

def get_last_three_items(items):
    return items[len(items)-3:len(items)]

numbers = [1, 2, 3, 4, 5]
result = get_last_three_items(numbers)
print(result)  # Works fine: [3, 4, 5]

small_list = [1, 2]
result = get_last_three_items(small_list)
print(result)  # Works but unexpected: [1, 2]

empty_list = []
result = get_last_three_items(empty_list)
print(result)  # [] - Is this what we want?

Debugging process:

1. Test edge cases:

print(get_last_three_items([1, 2, 3, 4, 5]))  # [3, 4, 5] ✓
print(get_last_three_items([1, 2]))           # [1, 2] - Only 2 items
print(get_last_three_items([1]))              # [1] - Only 1 item
print(get_last_three_items([]))               # [] - Empty

2. Decide on expected behavior: Should we raise an error if there aren't three items? Return what's available?

3. Fix with clear behavior:

def get_last_three_items(items):
    if len(items) < 3:
        return items  # Return all items if fewer than 3
    return items[-3:]  # Simpler syntax for last 3 items

Example 3: The Type Confusion

Problem:

def calculate_discounted_price(price, discount):
    return price - (price * discount)

product_price = input("Enter price: ")  # User enters "100"
discount_percent = input("Enter discount %: ")  # User enters "10"

final_price = calculate_discounted_price(product_price, discount_percent)
print(f"Final price: ${final_price}")

Error:

Debugging process:

1. Check types:

product_price = input("Enter price: ")
discount_percent = input("Enter discount %: ")

print(f"Price type: {type(product_price)}")       # <class 'str'>
print(f"Discount type: {type(discount_percent)}") # <class 'str'>

2. Identify the problem: input() always returns strings. You can't do math with strings.

3. Fix with type conversion and validation:

def calculate_discounted_price(price, discount):
    return price - (price * discount / 100)

try:
    product_price = float(input("Enter price: "))
    discount_percent = float(input("Enter discount %: "))
    
    final_price = calculate_discounted_price(product_price, discount_percent)
    print(f"Final price: ${final_price:.2f}")
except ValueError:
    print("Please enter valid numbers")

How to Ask for Help (When You're Stuck)

Sometimes you'll be genuinely stuck. Here's how to get help effectively.

What to Include When Asking for Help

1. The error message (full traceback)

Traceback (most recent call last):
  File "main.py", line 15, in <module>

2. The relevant code Not your entire program. Just the function/section causing the error.

3. What you've tried "I tried checking if the list was empty, but I still get the error."

4. What you expected vs. what happened "I expected the function to return the sum, but I'm getting None."

5. Your Python version Some errors are version-specific.

Where to Ask for Help

Stack Overflow

  • Search first (your error has probably been answered)

  • Follow their question format

  • Provide a minimal reproducible example

Python Discord/Reddit

  • Good for quick questions

  • More casual than Stack Overflow

  • Active communities

Official Python Documentation

  • Comprehensive error explanations

  • Standard library documentation

Your Action Plan: Becoming a Better Debugger

Here's how to build debugging skills systematically.

Week 1: Master Error Messages

  1. When you get an error, read the full traceback

  2. Look up each error type you encounter

  3. Keep a log of errors and solutions

Week 2: Practice Print Debugging

  1. Add print statements before assuming what's wrong

  2. Print variable types, not just values

  3. Use descriptive labels in print statements

Week 3: Test Edge Cases

  1. For every function, test with empty inputs

  2. Test with single items

  3. Test with unexpected types

Week 4: Learn One Debug Tool

  1. Try pdb for interactive debugging

  2. Practice using breakpoints

  3. Explore variable inspection

Ongoing: Build Pattern Recognition

  • Keep a debugging journal

  • Note which errors appear most often

  • Document your solutions

Common Debugging Myths to Ignore

Myth: "Good programmers don't get errors."

False. Everyone gets errors. The difference is speed of resolution.

Myth: "Debugging is just trial and error."

False. Debugging is systematic problem-solving.

Myth: "You need fancy tools to debug effectively."

False. Print statements and careful reading solve 90% of bugs.

Myth: "Errors mean you're bad at programming."

False. Errors mean you're learning. They're data, not judgments.

Final Thoughts: Debugging Is a Core Skill

You're learning Python. Debugging isn't a tangent from that journey. It's central to it.

Every error you encounter and solve makes you better. You build pattern recognition. You develop intuition. You become self-sufficient.

The goal isn't to write perfect code that never breaks. The goal is to write code that breaks in understandable ways, and to have the skills to fix it quickly.

Professional developers don't write bug-free code. They write code, it breaks, they debug it, they fix it. Over and over. That's the job.

You're not behind because you're getting errors. You're learning because you're getting errors.

Start with the fundamentals: read the error message, understand what it means, use print statements, test your assumptions. These simple techniques will solve most of your debugging challenges.

The rest comes with practice and pattern recognition. Keep coding. Keep debugging. Keep learning.

Every error is an opportunity to understand your code better.

Written by Julian Arden

Written by Julian Arden

Subscribe to my
newsletter

Get new travel stories, reflections,
and photo journals straight to your inbox

By subscribing, you agree to the Privacy Policy

Subscribe to my
newsletter

Get new travel stories, reflections,
and photo journals straight to your inbox

By subscribing, you agree to the Privacy Policy

Subscribe
to my

newsletter

Get new travel stories, reflections,
and photo journals straight to your inbox

By subscribing, you agree to the Privacy Policy