Debugging is the process of finding out why a program does something different from what you expected, and fixing the cause. The reliable way to do it is systematic: reproduce the problem, read the evidence (tracebacks and output), form a hypothesis, inspect the program's state to test it, then fix and confirm.
def distance_km(meters):
return meters / 1000
breakpoint() # pauses here and opens the debugger
print(distance_km(2750))This lesson covers each step, including Python's built-in debugger, pdb, which breakpoint() opens. The examples investigate a route planner for a cycling club that reports wrong distances.
Reproduction
Before changing any code, make the bug happen on demand. Record the exact input, what you expected and what you actually got:
Input: route "canal loop", 2750 metres
Expected: 2.75 km
Actual: 2750.0 kmThen shrink the example: remove unrelated code and data until you have the smallest program that still shows the bug. A five-line reproduction is far easier to reason about than a whole application, and often the act of shrinking it reveals the cause.
Change one thing at a time. If you change the input and the code together and the result changes, you can't tell which change mattered. Once you have a reliable reproduction, turn it into an automated test; it will tell you when the fix works and catch the bug if it ever returns.
Tracebacks
When an exception isn't handled, Python prints a traceback showing the chain of calls that led to it, most recent call last:
Traceback (most recent call last):
File "routes.py", line 12, in <module>
print(total_km(stages))
File "routes.py", line 8, in total_km
return sum(distance_km(s) for s in stages)
File "routes.py", line 8, in <genexpr>
return sum(distance_km(s) for s in stages)
File "routes.py", line 2, in distance_km
return meters / 1000
TypeError: unsupported operand type(s) for /: 'str' and 'int'Read it from the bottom up: the last line is the exception type and message, the frame above it is where it happened, and the frames above that show how the program got there. Python 3.11 and later also underline the exact part of the line that failed.
The line that raises the error isn't always the line that's wrong. Here distance_km is fine; the bug is that one of the stages values is a string, probably read from a file without conversion. Follow the bad value back up the frames to where it came from.
Breakpoints
breakpoint() (Python 3.7+) pauses the program and opens the debugger at that line, so you can inspect variables and step through code:
| Command | Action |
|---|---|
p expr | print the value of an expression |
pp expr | pretty-print it |
l / ll | list code around the current line / the whole function |
n | run the next line, stepping over function calls |
s | step into a function call |
c | continue until the next breakpoint |
w | show where you are in the call stack |
u / d | move up or down the stack |
q | quit |
python -m pdb script.py starts a program under the debugger, and python -m pdb -c continue script.py stops automatically where an uncaught exception occurs, so you can inspect the state at the moment of failure. Graphical debuggers in VS Code and PyCharm do the same with clickable breakpoints and variable panels.
Stepping executes real code: if the next line sends an email or writes a file, n will do it. Set PYTHONBREAKPOINT=0 to disable leftover breakpoint() calls, and remove them before committing.
Hypotheses
Random edits ("let me try adding int() here") may hide a symptom while leaving the real cause in place. Instead, state a hypothesis as a prediction you can test:
"The distances are 1,000 times too large because
distance_kmis called with values already in kilometres."
If that's true, printing the input at the start of distance_km should show 2.75, not 2750. Check it with a print, a log line, an assertion or the debugger. If the evidence contradicts the hypothesis, discard it and form another; if it confirms it, fix that cause.
Useful tools for gathering evidence: print(f"{value=}") shows a name and its value, type(value) and repr(value) reveal hidden types and whitespace, assert states assumptions in code, and logging keeps diagnostic messages you can switch on and off. Explaining the problem out loud to someone, or to a rubber duck, forces you to state your assumptions, and often reveals the wrong one.
Independent practice and a worked check
Task: reduce a wrong route distance to a small example and inspect the units before fixing the calculation.
def distance_km(meters):
return meters / 1000
route_km = [2.75, 1.8, 3.1] # already in kilometres
print(sum(distance_km(d) for d in route_km)) # 0.00765: wrong
print([f"{d=}" for d in route_km][:1]) # evidence: values are km, not m
print(round(sum(route_km), 2)) # 7.65: the correct totalThe evidence shows the values are kilometres, so they must not be converted again. For the worked check, a useful debugging hypothesis is a prediction you can test: it says what you'll observe if it's right, so the evidence can confirm or reject it.
Reference and next practice
The documentation covers pdb and breakpoint(). Continue with logging and testing with pytest. Practise with the Python exercises and Python quiz.
Debugging Python Programs Summary
- Reproduction: A small repeatable example gives an investigation a stable starting point.
- Tracebacks: A traceback shows the active call chain and the exception location.
- Breakpoints: breakpoint enters the configured debugger so execution state can be inspected.
- Hypotheses: A focused hypothesis predicts what evidence would confirm or reject a cause.
Debugging Python Programs Examples
Each example shows the code, the output it produces and what each part does.
Debugging Python Programs in a small program
The fixed version: once the input is confirmed to be in metres, the conversion is correct.
def distance_km(meters):
return meters / 1000
measured = 2750
print(distance_km(measured))2.75Line-by-line explanation
- Lines 1 to 2The function expects metres and converts to kilometres.
- Lines 4 to 5Passing 2750 metres gives the expected 2.75 km.
Finding a unit bug with evidence
def distance_km(meters):
return meters / 1000
def load_stage(record):
# record looks like "canal loop,2.75 km" or "hill climb,1800 m"
name, raw = record.split(",")
value, unit = raw.split()
return name, float(value), unit
stages = ["canal loop,2.75 km", "hill climb,1800 m"]
for record in stages:
name, value, unit = load_stage(record)
print(f"{name=} {value=} {unit=}") # evidence before the fix
km = value if unit == "km" else distance_km(value)
print(f"{name}: {km} km")name='canal loop' value=2.75 unit='km'
canal loop: 2.75 km
name='hill climb' value=1800.0 unit='m'
hill climb: 1.8 kmLine-by-line explanation
- Line 13The
=specifier prints each name with its value, revealing that the data mixes units. - Line 14The fix converts only values recorded in metres, instead of converting everything.
Common Debugging Python Programs Mistakes
Fixing the line that crashed instead of the cause
def distance_km(meters):
return float(meters) / 1000 # hides that bad data reached this functiondef load_stage(raw_meters):
return float(raw_meters) # convert where text enters the program
def distance_km(meters):
return meters / 1000Trace the bad value back to where it was created and fix it there.
Leaving breakpoint() calls in committed code
def total_km(stages):
breakpoint() # stops the program for every user
return sum(stages)def total_km(stages):
return sum(stages)Search for breakpoint( before committing, or let a linter flag it.
Debugging Python Programs in Real Projects
Debugging in professional work
Developers spend a large share of their time debugging, and the systematic approach scales from scripts to large systems. Production bugs start from logs and error-tracking tools such as Sentry, which capture tracebacks with the values of local variables. The first step is still to reproduce the bug locally with the same input, then write a failing test.
git bisect finds the commit that introduced a bug by testing old versions in a binary search, which turns "it used to work" into a precise answer. Remote debuggers attach to running services, and python -X faulthandler prints tracebacks even when the interpreter crashes. The habits from this lesson, evidence first and one change at a time, apply to all of them.
Debugging Python Programs FAQ
How do I debug Python code step by step?
Put breakpoint() on the line where you want to pause, run the program, then use n to execute the next line, s to step into a function, p name to print a variable and c to continue. Editors such as VS Code offer the same features with clickable breakpoints.
What is pdb in Python?
pdb is Python's built-in command-line debugger. It lets you pause a program, inspect and change variables, move through the call stack and execute code line by line. breakpoint() opens it by default.
Is print debugging bad?
No. Printing values is quick and effective for many bugs, especially with f-string debugging such as print(f"{total=}"). Switch to the debugger when you need to explore state interactively, and to logging when diagnostic messages should stay in the code.
How do I read a Python traceback?
Start at the bottom: the last line gives the exception type and message. The frame just above it shows the file, line and code where it happened, and the frames above that show the calls that led there, oldest at the top.
How do I debug an error that only happens sometimes?
Record the inputs and environment whenever it happens, using logging, then look for what differs between failing and passing runs: particular data, timing, order or configuration. Once you find a reliable trigger, write a test that reproduces it before fixing.
Debugging Python Programs Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
What makes a useful debugging hypothesis?
Which review concern is valid for debugging python programs?
Debugging Python Programs Quiz
2 questions to check what you learned.
What makes a useful debugging hypothesis?
- A random code change
- A prediction you can test
- Python ignores the distinction
Which statement correctly describes breakpoints?
- breakpoint enters the configured debugger so execution state can be inspected.
- Only the example output matters, not the rule
- Every operation has the same behavior