Skip to content

Python Debugging Tips for Beginners

Updated 9 min read Aimerscrew

A bug is any mistake that makes a program behave differently from what you intended. Every programmer writes bugs; the skill that separates beginners from experienced developers is how quickly they find them. Python debugging is that skill, and it's very learnable. In this lesson, you'll learn a simple process and a set of tools, from humble print() calls to a real step-by-step debugger, plus a checklist of the bugs beginners hit most often.

🌍 Real-life example

Think of a mechanic with a car that makes a strange noise. They don't replace random parts and hope. They take the car for a drive to hear the noise again, check the dashboard lights, form a guess ("the brakes?"), test that one guess, fix it, and drive again to confirm. Good debugging is exactly this calm, step-by-step process.

What is debugging in Python?

Debugging means finding the cause of a bug and fixing it. The word goes back to the early days of computing, when engineers really did pull insects out of machines. Today, "bugs" come in the three types you met in types of errors in Python: syntax errors, exceptions and logic errors. The first two come with an error message. Logic errors don't, which is why the tools in this lesson matter.

The debugging loop

Whatever the bug, the same loop works:

Animation of the debugging loop: reproduce the bug, read the error, guess the cause, test the guess with print or a debugger, fix one thing, re-run and verify, then go round again if it is still broken
Debugging is a loop: reproduce, read, guess, test, fix, verify. Go round again until the bug is gone.
  1. Reproduce it. Find the exact input that makes the bug happen every time.
  2. Read the error. If there's a traceback, start at the last line.
  3. Guess the cause. Which line, and which variable, could be wrong?
  4. Test the guess. Look at the real values with print() or a debugger.
  5. Fix one thing. Change a single thing, so you know what fixed it.
  6. Re-run and verify. Check the original case and a few others.

Tip 1: Read the error message carefully

When there is a traceback, it's a gift. The last line tells you the error type and message, and the frame above it tells you the file and line (see how to read a traceback). Messages like NameError: name 'totl' is not defined. Did you mean: 'total'? often hand you the fix directly. Read the whole message before touching the code.

Tip 2: Print debugging

The simplest debugging tool is print(). Most bugs come from a variable holding a different value than you think it holds, so print it and look. This function should average three temperatures to 22, but it doesn't:

def average_temperature(temps):
    total = 0
    for i in range(1, len(temps)):
        total = total + temps[i]
    return total / len(temps)

print(average_temperature([20, 22, 24]))
15.333333333333334

No error, just a wrong answer: a logic error. Let's print what happens inside the loop:

def average_temperature(temps):
    total = 0
    for i in range(1, len(temps)):
        print(f"{i=}, {temps[i]=}, {total=}")
        total = total + temps[i]
    return total / len(temps)

print(average_temperature([20, 22, 24]))
i=1, temps[i]=22, total=0
i=2, temps[i]=24, total=22
15.333333333333334

Now it's obvious: the loop starts at index 1, so the first temperature (20) is never added. Changing range(1, len(temps)) to range(len(temps)) fixes it. Remove the debug print once you're done.

The f"{x=}" shortcut

Did you notice {i=} in that f-string? Adding = after an expression in an f-string prints the expression's text and its value. It saves typing labels by hand, and it works with whole expressions and format specs too:

price = 19.99
quantity = 3
discount = 0.1

print(f"{price=}, {quantity=}")
print(f"{price * quantity=}")

total = price * quantity * (1 - discount)
print(f"{total=:.2f}")
price=19.99, quantity=3
price * quantity=59.97
total=53.97

💡 Tip

Print the type as well as the value when something looks right but behaves wrong. print(type(age)) showing <class 'str'> instantly explains why age + 1 fails: you forgot to convert the result of input().

Tip 3: Use assert to check your assumptions

An assert statement checks that something you believe is true really is. If the condition is False, Python raises an AssertionError with your message:

def apply_discount(price, percent):
    assert 0 <= percent <= 100, f"percent must be 0-100, got {percent}"
    return price * (1 - percent / 100)

print(apply_discount(80, 25))
print(apply_discount(80, 250))
60.0
AssertionError: percent must be 0-100, got 250

Asserts catch bugs close to where they start instead of three functions later. They are for checking your own code's assumptions while developing. Python can be run with a flag (-O) that skips all asserts, so for checking user input, raise an exception instead.

Tip 4: Pause your program with breakpoint() and pdb

Print debugging means editing and re-running over and over. A debugger lets you pause the program at any line, look at every variable, and run the code one line at a time. Python has one built in, called pdb (Python debugger). Start it by adding breakpoint() where you want to stop:

def cart_total(prices, discount):
    total = sum(prices)
    breakpoint()
    total = total - discount
    return total


print(cart_total([12.5, 8.0, 4.5], 5))

When you run this file (called checkout.py here), the program pauses and shows a (Pdb) prompt. You type commands; here the user typed p total, p prices, n, p total and c:

> checkout.py(4)cart_total()
-> total = total - discount
(Pdb) p total
25.0
(Pdb) p prices
[12.5, 8.0, 4.5]
(Pdb) n
> checkout.py(5)cart_total()
-> return total
(Pdb) p total
20.0
(Pdb) c
20.0

The line after -> is the next line to run, not one that already ran. On your computer, the file path will be longer.

pdb commandWhat it does
p exprPrint the value of a variable or expression, like p total
n (next)Run the current line and stop at the next one
s (step)Like n, but step into a function call
c (continue)Keep running until the next breakpoint or the end
l (list)Show the code around the current line
w (where)Show the call stack, like a traceback
q (quit)Stop the program and leave the debugger
h (help)List all commands

Don't forget to delete breakpoint() when you're finished, or the program will keep pausing.

Tip 5: Use the VS Code debugger

If you use VS Code with the Python extension, you get the same powers with a mouse:

  1. Set a breakpoint: click in the margin just left of a line number. A red dot appears.
  2. Start debugging: press F5 (or open Run and Debug) and choose Python File.
  3. Inspect: when the program pauses at the red dot, the Variables panel shows every variable's current value. Hover over a name in the code to see its value too.
  4. Step: F10 runs the next line (step over), F11 goes inside a function (step into), and F5 continues to the next breakpoint.

Other editors, like PyCharm and Thonny, have nearly identical debuggers. Learning one makes the others easy.

Tip 6: Isolate the problem and explain it out loud

When a bug hides in a big program, shrink the search area:

  • Test pieces separately. Call one function with simple values you can check by hand, like average_temperature([10, 20]), which should give 15.0.
  • Comment out half. Temporarily turn half the code into comments. If the bug disappears, it's in that half. Repeat.
  • Rubber duck debugging. Explain your code, line by line, to someone else or even a rubber duck on your desk. Saying "and then this line adds the price..." out loud very often makes you spot the mistake yourself.
  • Take a break. Seriously. Fresh eyes find in two minutes what tired eyes missed for an hour.

Common beginner bugs checklist

Before you dig deep, check this list. These cause a huge share of beginner bugs:

BugSymptomFix
Forgot returnFunction gives NoneAdd return value
Forgot () when callingPrints <function greet at 0x...>Write greet()
Off-by-one in range() or indexesFirst or last item skippedRemember indexes start at 0 and range stops before the end
input() not convertedTypeError or "5" + "5" gives "55"Wrap it in int() or float()
Wrong indentationLine runs every loop instead of once, or the reverseCheck which block each line belongs to
= instead of ==SyntaxError in an ifUse == to compare
Comparing floats with ==0.1 + 0.2 == 0.3 is FalseUse round() or compare with a small tolerance
Naming a variable list, sum or maxBuilt-in stops working laterChoose a different name

Here's the first one in action. The function calculates the price with tax but never hands it back:

def add_tax(price):
    total = price * 1.2

print(add_tax(50))
None

Adding return total as the last line of the function fixes it, and the program prints 60.0.

Common mistakes while debugging

1. Changing many things at once

If you change five lines and the bug goes away, you don't know which change fixed it, or whether one of them created a new bug. Change one thing, then re-run.

2. Assuming instead of checking

"That variable is definitely a number" is exactly the kind of belief that hides bugs. Print it or inspect it in the debugger. Let the program tell you the truth.

3. Leaving debug code behind

Stray print() calls clutter the output, and a forgotten breakpoint() freezes the program for the next person. Clean up when the bug is fixed.

4. Only testing the case that failed

After the fix, also try normal values, edge cases (an empty list, zero, a single item) and the original failing input.

Practice exercise

🏋️ Try it yourself

This function should count how many orders are over $50, but for the list below it prints 1 instead of 2. Use print debugging with f"{...=}" to find the bug, then fix it.

def count_big_orders(orders):
    count = 0
    for amount in orders:
        if amount > 50:
            count = count + 1
        return count

print(count_big_orders([75, 20, 120, 45]))
1
Show solution

Adding print(f"{amount=}, {count=}") as the first line inside the loop shows only one line of output: amount=75, count=0. The loop stops after the first order because return count is indented inside the loop. Move it out one level:

def count_big_orders(orders):
    count = 0
    for amount in orders:
        if amount > 50:
            count = count + 1
    return count

print(count_big_orders([75, 20, 120, 45]))
2

What's next?

That wraps up errors and exceptions. You can read tracebacks, handle and raise errors, design your own, and debug with confidence. Next, start the File Handling chapter and learn to read files in Python, where these skills will come in handy straight away.

Key takeaways

  • Debugging is a loop: reproduce the bug, read the error, guess the cause, test the guess, fix one thing and re-run.
  • Most bugs are variables holding different values than you think, so print them and check.
  • Use f"{x=}" to print both an expression and its value, for example f"{total=}".
  • assert checks your own assumptions during development; use raise for validating user input.
  • breakpoint() pauses the program in pdb, where p, n, s, c and q let you inspect and step through code.
  • The VS Code debugger does the same with red-dot breakpoints, a Variables panel and F10/F11 stepping.
  • Check the common beginner bugs first: missing return, missing (), off-by-one, unconverted input and wrong indentation.

Frequently asked questions

How do I debug Python code as a beginner?

Read the error message first, starting from the last line of the traceback. Then print the variables involved, or pause the program with breakpoint() or your editor's debugger, and compare their real values with what you expected.

What does f"{x=}" do in Python?

It is an f-string shortcut, available since Python 3.8, that prints the expression text followed by its value, such as x=5. It is handy for quick print debugging.

How do I use breakpoint() in Python?

Add breakpoint() on the line where you want to pause and run the program. You get a (Pdb) prompt where p name prints a variable, n runs the next line, s steps into a function, c continues and q quits.

What is the difference between assert and raise?

assert checks an assumption in your own code and can be turned off with python -O. raise is for real error handling, like rejecting invalid user input, and is never turned off.

How do I set a breakpoint in VS Code for Python?

Click in the margin to the left of a line number so a red dot appears, then press F5 and choose Python File. The program pauses at the dot so you can inspect variables and step with F10 and F11.

What is rubber duck debugging?

It means explaining your code line by line out loud, even to a rubber duck. Putting your thinking into words often reveals the mistake without any other help.