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.
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:
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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
It prints 56.666… instead, and no error tells you why. Before you change anything:
- 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.
- 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.
- Form a guess and check it, as below. Change one thing at a time, and run the program again after each change.
Print the values that matter
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:
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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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:
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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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.pand an expression: print its value, as inp totalorp 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:
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:
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:
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’sr: 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 aprint()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.
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:nruns a line,ssteps into a call,pprints a value,ccontinues,wshows where you are andqquits.PYTHONBREAKPOINT=0turns everybreakpoint()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, soadd_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 = [].
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
References
- pdb, the Python debugger (Python Software Foundation)
- Built-in functions: breakpoint() (Python Software Foundation)
- sys.breakpointhook() (Python Software Foundation)
- Command line and environment: PYTHONBREAKPOINT (Python Software Foundation)
- PEP 553: Built-in breakpoint() (Python Software Foundation)
- What's New In Python 3.14: pdb (Python Software Foundation)
- f-strings: the debug specifier (Python Software Foundation)
- The Python Tutorial: default argument values (Python Software Foundation)
- IDLE: the Debug menu (Python Software Foundation)
- Debug code with Visual Studio Code (Microsoft)
- Python debugging in VS Code (Microsoft)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress