Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python Module 3 – Control flow: decisions, loops and pattern matching

Debugging with print, breakpoint() and pdb

Find bugs in Python with targeted print() calls and f-strings, pause a program with breakpoint(), step through it in pdb and use an editor's debugger.

  • Beginner
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot

What you will learn

  • Find bugs with targeted print statements
  • Use breakpoint() to pause a program and step through it in pdb
  • Use the debugger built into an editor

Before you start

On this page

A traceback tells you where a program stopped. Many bugs give you no traceback at all: the program runs to the end and prints a wrong answer. Debugging is the work of finding which line does something other than what you meant, and it goes fastest when you stop guessing and look at what the program actually does, one value at a time.

This lesson uses one small bug throughout. It finds the bug with print(), then with the debugger pdb in a terminal, and then shows where the same controls are in the debugger of an editor.

Start with a bug you can repeat

This function should return the average of a list of marks. For 70, 80 and 90 the answer is obviously 80:

An average that comes out wrong Python · average.py
def average(marks):
    total = 0
    i = 1
    while i < len(marks):
        total += marks[i]
        i += 1
    return total / len(marks)


print(average([70, 80, 90]))

Output

56.666666666666664

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 average.py

It prints 56.666… instead, and no error tells you why. Before you change anything:

  1. Make the bug small and repeatable. Three marks whose right answer you can work out by hand beat a real class list: you know what every intermediate value should be.
  2. Explain the code line by line. Say, out loud or in writing, what each line does with these marks. Explaining to someone, or to a rubber duck on your desk, makes you read what is written instead of what you meant to write, and many bugs turn up at this step.
  3. Form a guess and check it, as below. Change one thing at a time, and run the program again after each change.

The quickest check is a print() where you suspect the problem. Print the values the next line depends on, with a label, so the output says what each number is:

The same function with targeted prints Python · average_print.py
def average(marks):
    total = 0
    i = 1
    while i < len(marks):
        print(f"before adding: {i=} {marks[i]=} {total=}")
        total += marks[i]
        i += 1
    print(f"after the loop: {total=} {len(marks)=}")
    return total / len(marks)


print(average([70, 80, 90]))

Output

before adding: i=1 marks[i]=80 total=0
before adding: i=2 marks[i]=90 total=80
after the loop: total=170 len(marks)=3
56.666666666666664

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 average_print.py

The output shows the loop adding 80 and 90 and never 70: i starts at 1, so the first mark, at position 0, is skipped. The fix is i = 0. Delete the prints once the bug is fixed, or they will clutter every later run.

The = inside the braces of an f-string does the labelling for you: f"{total=}" prints the expression, an equals sign and the value. The spaces you write around = appear in the output too, and any expression works, not only a name:

f-strings with = for quick checks Python · fstring_debug.py
price = 249.5
quantity = 3
name = "Notebook "

print(f"{price=}")
print(f"{quantity = }")
print(f"{price * quantity = }")
print(f"{price * quantity = :.2f}")
print(f"{name=}")

Output

price=249.5
quantity = 3
price * quantity = 748.5
price * quantity = 748.50
name='Notebook '

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fstring_debug.py

Without a format such as :.2f, the value is shown the way repr() shows it, so strings get quotes and the space at the end of "Notebook " becomes visible. Invisible characters at the ends of text are a common reason why two strings that look equal are not.

Version note

The = specifier of f-strings has been part of Python since 3.8.

Pause the program with breakpoint()

Prints show what you thought to ask for. A debugger lets you stop the program and ask anything. Python’s own debugger, pdb, is in the standard library: put breakpoint() on a line of its own where you want the program to stop, and run the program in a terminal as usual. When Python reaches the call, pdb shows where the program has stopped and waits at its prompt, (Pdb), for commands:

  • n (next): run the current line, including any function it calls, and stop at the next line here.
  • s (step): run the current line, but stop inside a function it calls.
  • c (continue): run on until the next breakpoint or the end of the program.
  • r (return): run on until the current function returns.
  • p and an expression: print its value, as in p total or p marks[i].
  • ll (longlist): list the source code of the current function.
  • w (where): show the chain of calls that led to the current line.
  • q (quit): stop the program.

Here is the average function again, with breakpoint() just before the line that calls it:

average_debug.py: the same program with a breakpoint Python · pdb_demo/average_debug.py
def average(marks):
    total = 0
    i = 1
    while i < len(marks):
        total += marks[i]
        i += 1
    return total / len(marks)


marks = [70, 80, 90]
breakpoint()
print(average(marks))

pdb needs a person at a terminal to type the commands, which a web page cannot offer. So the session below was recorded by a short program, session.py, which runs python3 average_debug.py exactly as you would and types the commands for you. Its output is what you would see on screen, with two differences: pdb prints the whole path of the file where this page shows only its name, and a terminal adds colours:

A pdb session on the average function Python · pdb_demo/session.py
import re
import subprocess
import sys

# What you would type, in order: commands at each (Pdb) prompt, then y to confirm q.
typed = ["n", "s", "n", "n", "n", "p i, marks[i]", "p marks[0]", "w", "q", "y"]

# Run the program the way `python3 average_debug.py` does, with the typing done for you.
result = subprocess.run(
    [sys.executable, "average_debug.py"],
    input="\n".join(typed) + "\n",
    capture_output=True,
    text=True,
)

# Without a terminal, nothing shows what was typed, so put each line back after its prompt.
answers = iter(typed)
print("$ python3 average_debug.py")
for part in re.split(r"(\(Pdb\) |\[y/n\] )", result.stdout):
    if part in ("(Pdb) ", "[y/n] "):
        print(part + next(answers))
    elif part.strip():
        print(part, end="")

Output

$ python3 average_debug.py
> average_debug.py(11)<module>()
-> breakpoint()
(Pdb) n
> average_debug.py(12)<module>()
-> print(average(marks))
(Pdb) s
--Call--
> average_debug.py(1)average()
-> def average(marks):
(Pdb) n
> average_debug.py(2)average()
-> total = 0
(Pdb) n
> average_debug.py(3)average()
-> i = 1
(Pdb) n
> average_debug.py(4)average()
-> while i < len(marks):
(Pdb) p i, marks[i]
(1, 80)
(Pdb) p marks[0]
70
(Pdb) w
  average_debug.py(12)<module>()
-> print(average(marks))
> average_debug.py(4)average()
-> while i < len(marks):
(Pdb) q
Quitting pdb will kill the process. Quit anyway? [y/n] y

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 session.py

Each stop shows the file, the line number in brackets and the function (<module> for code outside any function), then, after ->, the current line: the one that runs next. The session uses n to reach the call, s to step into average() (pdb prints --Call-- as it enters the function), and n three more times to reach the loop. There p i, marks[i] shows that the loop starts at position 1, with 80, while p marks[0] shows the 70 that will never be added. w lists the two frames: the line of the file that called average(), and the current line inside it, marked with >.

A line that is not a pdb command runs as Python in the program, so total on its own shows the value of total, and total = 0 would change it. When a variable has the same name as a command, such as n, write p n or put ! in front of the line.

Version note

Since Python 3.13, breakpoint() stops on its own line, as the session shows, instead of the line after it. Python 3.14 colours the source lines pdb shows in a terminal, indents multi-line input for you, and asks for confirmation before q ends a program that stopped at breakpoint(): y, Enter or the end of the input confirm.

Turn breakpoints off

breakpoint() calls sys.breakpointhook(), which first reads the environment variable PYTHONBREAKPOINT. Set it to 0 and every breakpoint() call does nothing, so you can run the program normally without editing it:

Running past breakpoint() with PYTHONBREAKPOINT=0 Python · pdb_demo/no_breakpoints.py
import os
import subprocess
import sys

# The same program with the environment variable PYTHONBREAKPOINT set to 0.
env = {**os.environ, "PYTHONBREAKPOINT": "0"}
result = subprocess.run([sys.executable, "average_debug.py"], env=env, capture_output=True, text=True)

print("$ PYTHONBREAKPOINT=0 python3 average_debug.py")
print(result.stdout, end="")

Output

$ PYTHONBREAKPOINT=0 python3 average_debug.py
56.666666666666664

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 no_breakpoints.py

Remove breakpoint() calls once you have fixed the bug. When a program runs where nobody can type, as a scheduled job or behind a Run button on a web page, pdb reads the end of the input, takes it as q, and the program ends on that line. To debug a program without editing it at all, start it with python3 -m pdb average.py: pdb stops before the first line, and you set breakpoints with b and a line number, then c to run to them.

The debugger in your editor

Editors put the same controls behind buttons and a panel of variables, which many people find easier than typing commands. In Visual Studio Code with its Python extension, which installs the Python Debugger extension with it:

  • Click in the margin to the left of a line number, or press F9, to set a breakpoint on that line; it shows as a red circle.
  • Press F5, or select Run and Debug in the Run and Debug view, then choose Python Debugger and Python File. The program runs until it reaches a breakpoint.
  • Step with F10 (step over, pdb’s n), F11 (step into, s) and Shift+F11 (step out, pdb’s r: run until the current function returns); F5 continues.
  • The Variables section shows the local and global variables of the frame you select, Watch shows expressions you choose each time the program stops, and Call Stack is pdb’s w.
  • A conditional breakpoint stops only when an expression is true, such as i == 2, and a logpoint writes a message to the Debug Console without stopping, like a print() that is never in your code, so there is nothing to delete afterwards.

Other editors offer the same ideas under similar names. IDLE, the editor in Python’s standard library (an optional part, which some Python installations leave out), has a simpler debugger: choose Debug, then Debugger, in the Shell window, then run your file from the editor; IDLE’s documentation calls it incomplete and somewhat experimental. No lesson of this track needs a particular editor, and pdb works wherever Python runs in a terminal.

Debugging in the browser

The Run buttons of these lessons and the Python Online Compiler give a program its input before it starts and show what it prints, so there is no terminal for pdb to wait at. In the browser, debug with print() and f"{name=}"; save pdb and your editor’s debugger for Python on your own computer.

Python Online Compiler Paste average_print.py, fix the starting index, and check that the prints and the result agree. Text Diff Compare the output your program prints with the output you expected, line by line.

Key takeaways

  • Debug a small input whose right answer you know, explain the code line by line, and change one thing at a time.
  • Print the values the suspect line depends on, with labels; f"{expression=}" writes the label for you and shows strings with quotes, so stray spaces show up.
  • breakpoint() stops the program in pdb: n runs a line, s steps into a call, p prints a value, c continues, w shows where you are and q quits.
  • PYTHONBREAKPOINT=0 turns every breakpoint() off; remove them when the bug is fixed.
  • An editor’s debugger offers the same with breakpoints in the margin, step buttons and a panel of variables.

Exercise

Exercise · Easy · Python

Find and fix three bugs

The starter code has three short functions, and each one has exactly one bug. Each docstring says what the function should do. Find the bugs with the methods of this lesson (call the function with a small input and print what you get, or add breakpoint() when you run it on your own computer), then fix each one with the smallest change you can. Keep the names and parameters as they are: the tests call the functions with them.

  • sum_to(n) should return 1 + 2 + … + n, and 0 when n is 0. sum_to(4) is 10.
  • average_word_length(sentence) should return the average length of the words of a sentence that has at least one word. average_word_length("to be or not") is 2.25.
  • add_item(item, basket) should add the item to the basket it is given, even an empty one, and return that basket. Called without a basket, it should start a new one, so add_item("apple") returns ["apple"] every time.

The third bug is about a default value. Python works out a parameter's default once, when def runs, not at each call, so a list used as a default is one list that every call without a basket shares.

Starter code · bugs.py

def sum_to(n):
    """Return 1 + 2 + ... + n (0 when n is 0)."""
    total = 0
    k = 1
    while k < n:
        total += k
        k += 1
    return total


def average_word_length(sentence):
    """Return the average length of the words of a sentence that has at least one word."""
    words = sentence.split()
    total = 0
    for word in words:
        total += len(word)
    return total / len(sentence)


def add_item(item, basket=[]):
    """Add item to basket and return the basket; start a new basket when none is given."""
    basket.append(item)
    return basket
The sample tests · test_bugs.py
from bugs import add_item, average_word_length, sum_to


def test_sum_to():
    """adds every number from 1 to n, n included"""
    assert sum_to(4) == 10
    assert sum_to(1) == 1
    assert sum_to(10) == 55


def test_sum_to_zero():
    """returns 0 for n = 0"""
    assert sum_to(0) == 0


def test_average_word_length():
    """divides the letters by the number of words"""
    assert average_word_length("to be or not") == 2.25
    assert average_word_length("hello") == 5.0
    assert average_word_length("ab cd") == 2.0


def test_add_item_to_a_basket():
    """adds to the basket it is given and returns that same list"""
    basket = ["bread"]
    assert add_item("milk", basket) is basket
    assert basket == ["bread", "milk"]


def test_add_item_to_an_empty_basket():
    """adds to an empty basket it is given, instead of starting a new one"""
    basket = []
    assert add_item("milk", basket) is basket
    assert basket == ["milk"]


def test_add_item_new_basket_each_time():
    """starts a new basket for every call without one"""
    assert add_item("apple") == ["apple"]
    assert add_item("pear") == ["pear"]
A hint

Print what each function returns for a small input you can check by hand: sum_to(1), average_word_length("ab cd") (2.0) and two calls of add_item() without a basket. For the loop, print k and total on every pass and see which number is missing. For the average, ask what the division is dividing by. For the basket, the usual fix is a default of None and, inside the function, if basket is None: followed by basket = [].

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 What does fstring_debug.py print?

    What does this program print? Choose one answer.

    price = 249.5
    quantity = 3
    name = "Notebook "
    
    print(f"{price=}")
    print(f"{quantity = }")
    print(f"{price * quantity = }")
    print(f"{price * quantity = :.2f}")
    print(f"{name=}")
    Show the answer to question 1

    Answer: it prints

    price=249.5
    quantity = 3
    price * quantity = 748.5
    price * quantity = 748.50
    name='Notebook '

    With =, an f-string prints the expression as written, spaces included, then its value. Without a format specifier the value is shown with repr(), which puts quotes around the string and so shows the space at its end; with :.2f it is formatted with two decimals instead.

  2. Question 2 of 6 pdb has stopped on the line print(average(marks)). Which command takes you to the first line inside average()?

    Choose one answer.

    Show the answer to question 2

    Answer: s (step)

    s runs the current line but stops at the first chance, which is inside the function the line calls. n would run the whole call and stop at the next line of the current code, and c runs on until the next breakpoint or the end.

  3. Question 3 of 6 How do you run a program without stopping at any of its breakpoint() calls, without editing the code?

    Choose one answer.

    Show the answer to question 3

    Answer: Set the environment variable PYTHONBREAKPOINT to 0 when you run it

    breakpoint() calls sys.breakpointhook(), which reads PYTHONBREAKPOINT first; when it is 0, the call does nothing. python3 -m pdb starts the program in the debugger instead, and q ends the program.

  4. Question 4 of 6 pdb has stopped inside a function that has a variable called total. Which of these show its value?

    Choose every answer that is right.

    Show the answer to question 4

    Answer:

    • p total
    • print(total)
    • total

    p prints the value of an expression. A line that is not a pdb command runs as Python in the current frame, so typing total shows its value as the interactive shell would, and print(total) prints it. n takes no argument: pdb answers n total with "Invalid argument" and runs nothing.

  5. Question 5 of 6 Put the steps of the debugging routine from this lesson in order.

    Give each item its position, from 1 (first).

    Show the answer to question 5

    Answer:

    1. Make the bug happen with a small input whose right answer you know
    2. Explain what each line does with that input
    3. Check the values that matter, with print() or pdb
    4. Change one thing
    5. Run the program again and compare its answer with the right one

    With a small input whose answer you know, you can check every value by hand. Explaining the lines often shows the bug by itself; if it does not, the values you print or inspect in pdb show which line goes wrong. Changing one thing at a time and running the program after each change tells you which change fixed it.

  6. Question 6 of 6 In Visual Studio Code's debugger, which key runs the current line but stops inside a function that line calls, like pdb's s?

    Choose one answer.

    Show the answer to question 6

    Answer: F11

    F11 is Step Into, pdb's s. F10 is Step Over, like n; F5 starts the debugger or continues, like c; and F9 sets or removes a breakpoint on the current line.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.