Python debugging involves identifying and fixing errors in your code using tools like tracebacks, print()
calls, breakpoints, and tests. In this tutorial, you’ll learn how to interpret error messages, use print()
to track variable values, and set breakpoints to pause execution and inspect your code’s behavior. You’ll also explore how writing tests can help prevent errors and ensure your code runs as expected.
By the end of this tutorial, you’ll understand that:
- Debugging means identifying, analyzing, and resolving issues in your Python code using systematic approaches.
- Tracebacks are messages that help you pinpoint where errors occur in your code, allowing you to resolve them effectively.
- Using
print()
helps you track variable values and understand code flow, aiding in error identification. - Breakpoints let you pause code execution to inspect and debug specific parts, improving error detection.
- Writing and running tests before or during development aids in catching errors early and ensures code reliability.
Understanding these debugging techniques will empower you to handle Python errors confidently and maintain efficient code.
Get Your Code: Click here to download the free sample code that shows you how to debug common Python errors.
Take the Quiz: Test your knowledge with our interactive “How to Debug Common Python Errors” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
How to Debug Common Python ErrorsTake this quiz to review core Python debugging techniques like reading tracebacks, using print(), and setting breakpoints to find and fix errors.
How to Get Started With Debugging in Python
Debugging means to unravel what is sometimes hidden. It’s the process of identifying, analyzing, and resolving issues, errors, or bugs in your code.
At its core, debugging involves systematically examining code to determine the root cause of a problem and implementing fixes to ensure the program functions as intended. Debugging is an essential skill for you to develop.
Debugging often involves using tools and techniques such as breakpoints, logging, and tests to achieve error-free and optimized performance of your code. In simpler terms, to debug is to dig through your code and error messages in an attempt to find the source of the problem, and then come up with a solution to the problem.
Say you have the following code:
cat.py
print(cat)
The code that prints the variable cat
is saved in a file called cat.py
. If you try to run the file, then you’ll get a traceback error saying that it can’t find the definition for the variable named cat
:
$ python cat.py
Traceback (most recent call last):
File "/path_to_your_file/cat.py", line 1, in <module>
print(cat)
^^^
NameError: name 'cat' is not defined
When Python encounters an error during execution, it prints a traceback, which is a detailed message that shows where the problem occurred in your code. In this example, the variable named cat
can’t be found because it hasn’t been defined.
Here’s what each part of this Python traceback means:
Part | Explanation |
---|---|
Traceback (most recent call last) |
A generic message sent by Python to notify you of a problem with your code. |
File "/path_to_your_file/cat.py" |
This points to the file where the error originated. |
line 1, in <module> |
Tells you the exact line in the file where the error occurred. |
print(cat) |
Shows you the line of Python code that caused the error. |
NameError |
Tells you the kind of error it is. In this example, you have a NameError . |
name 'cat' is not defined |
This is the specific error message that tells you a bit more about what’s wrong with the piece of code. |
In this example, the Python interpreter can’t find any prior definition of the variable cat
and therefore can’t provide a value when you call print(cat)
. This is a common Python error that can happen when you forget to define variables with initial values.
To fix this error, you’ll need to take a step-by-step approach by reading the error message, identifying the problem, and testing solutions until you find one that works.
In this case, the solution would be to assign a value to the variable cat
before the print call. Here’s an example:
cat.py
cat = "Siamese"
print(cat)
Notice that the error message disappears when you rerun your program, and the following output is printed:
$ python cat.py
Siamese
The text string stored in cat
is printed as the code output. With this error resolved, you’re well on your way to quickly debugging errors in Python.
In the next sections, you’ll explore other approaches to debugging, but first, you’ll take a closer look at using tracebacks.
How to Debug Python Errors Using Tracebacks
As you’ve already seen, debugging with tracebacks is one of the ways to resolve bugs in your code. To use tracebacks, you need to be able to understand and process each line of the traceback, which is only possible when you understand it fully.
Understanding Tracebacks
A traceback is the general name for the message that’s printed on the screen when you encounter an error. Depending on the error and its source, the traceback may be just a few lines or stretch across many lines.
In the example below, you want to add two numbers but have forgotten to convert both to integers, so there’s an attempt to add a string and an integer:
>>> total = 23 + "3"
Traceback (most recent call last):
File "python-input-0", line 1, in <module>
total = 23 + "3"
~~~^~~~~
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Looking at the traceback above, you can see your code printed to the screen, with the caret symbol (^
) placed under the addition operator. This symbol highlights the position where the interpreter detects the problem.
When running the code, the interpreter encounters the addition operator between an integer and a string. These types can’t be added together in Python, so the interpreter raises a TypeError
.
Debugging With Tracebacks
To debug using a traceback, it’s important to carefully read each line of the error traceback log to understand and locate the source of the problem. In the traceback above, Python points to line 1 as the source of the error.
In the codebase of the file, scroll to the line that’s been pinpointed and check it against the follow-up message in the traceback to better understand what went wrong.
You can go ahead and resolve the error by converting the string to an integer, or in this case, by changing the string "3"
to the integer 3
:
>>> total = 23 + 3
>>> total
26
In this corrected version, changing the type of "3"
from a string to an integer resolves the error. If the string is stored in a variable, then you can use int()
to convert it to an integer.
It’s important to note that sometimes, depending on the cause of the bug, the error output may be a syntax error that doesn’t include the typical Traceback
keyword. For example, the code below attempts to add two integers but omits the addition operator, resulting in a SyntaxError
:
>>> total = 23 3
File "<python-input-0>", line 1
total = 23 3
^
SyntaxError: invalid syntax
Notice that the error log is similar to the traceback but varies slightly in structure. The same step-by-step approach you used to decode tracebacks also works when dealing with syntax errors.
How to Debug Python Errors With print()
When debugging with print()
, your aim is to print comments, expressions, or the values of variables at multiple points in your code until you locate the faulty line. You can also use it to keep track of variables and values as they move and change through your codebase. This approach is a simpler version of the more advanced technique called logging.
Understanding Debugging With print()
While you’re probably already familiar with Python’s built-in print()
function, it’s worth taking a closer look at how you can use it strategically to debug your code.
The print()
function is a built-in function that takes integers, strings, tuples, lists, and other objects, and prints them to the standard output—usually the terminal where you run your project.
In most cases, you use it to print output to the terminal when you’re unsure of the value a particular variable holds. In this way, you’re able to verify that the state of your code is exactly what you want at that moment. You can use the same technique when debugging.
Debugging With print()
In the following example, you have a function that checks for palindromes—words that read the same forward and backward:
>>> def find_palindromes(text):
... words = text.split()
... return [word for word in words if word == word[::-1]]
...
>>> find_palindromes("Dad plays many solos at noon, and sees a racecar.")
['solos', 'sees', 'a']
The function doesn’t find all the palindromes it’s supposed to return. In this case, it misses Dad
, noon
, and racecar
.
There are a couple of reasons for this. Dad
is missed because of capitalization, while noon
and racecar
are missed because of punctuation.
Here’s how you can debug the function using print()
:
-
Print
words
, the list of words obtained after splitting the text, to check if punctuation or case sensitivity is an issue. This will reveal that punctuation remains attached to some words, so they need to be cleaned. -
Print all words after they’ve been cleaned—that is, after converting them to lowercase and removing non-alphanumeric characters.
-
Print the list of words being checked for palindromes to confirm whether any matches are missing.
Here’s the updated code with print()
calls added:
>>> def find_palindromes(text):
... words = text.split()
... print(f"{words = }")
... # Remove punctuation and convert to lowercase
... normalized_words = [
... "".join(filter(str.isalnum, word)).lower()
... for word in words
... ]
... print(f"{normalized_words = }")
... palindromes = [
... word for word in normalized_words if word == word[::-1]
... ]
... print(f"{palindromes = }")
... return palindromes
...
>>> find_palindromes("Dad plays many solos at noon, and sees a racecar.")
words = ['Dad', 'plays', 'many', 'solos', (...), 'sees', 'a', 'racecar.']
normalized_words = ['dad', 'plays', 'many', (...), 'sees', 'a', 'racecar']
palindromes = ['dad', 'solos', 'noon', 'sees', 'a', 'racecar']
['dad', 'solos', 'noon', 'sees', 'a', 'racecar']
In this example, the calls to print()
in the highlighted lines reveal the values of the list at each step of the palindrome check. Notice how you take advantage of the self-documenting feature of f-strings to display the corresponding variable names. For better formatting, especially when dealing with lists that span multiple lines, you could swap out print()
for pprint()
to display the data in a more readable way.
Adding the filter()
for alphanumeric characters ensures that each word is stripped of non-alphanumeric attributes. Also, including .lower()
converts all the strings to lowercase for consistent comparisons.
Don’t forget to remove the print()
calls after you’ve resolved the errors, although you can always keep them in for logging or future debugging.
How to Debug Python Errors Using Breakpoints
Unlike print()
calls, which give you static snapshots of your code’s behavior, breakpoints let you pause execution and explore your program interactively while it’s running. This enables you to inspect the variables as they change along the way right up to the breakpoint.
In this section, you’ll explore how breakpoints work and how you can use them to investigate and fix bugs.
Understanding Breakpoints
A breakpoint is a marker or command in the code that signals the debugger to pause the program’s execution at a specific line. This allows you to inspect the program’s state and behavior at that moment.
By default, the interpreter executes code from top to bottom until it reaches the end of the file, encounters an error, or hits a breakpoint. When the program pauses at a breakpoint, you can execute a small part of the code and extend the sections bit by bit until you find the cause of the error.
In most code editors that support debugging, such as Visual Studio Code (VS Code) and PyCharm, breakpoints appear as visual markers, typically shown as small red dots next to the line numbers. You can add or remove breakpoints simply by clicking in the left margin beside the line of code you want to inspect.
Debugging With Breakpoints
When a program encounters a breakpoint, the debugger takes control of the program’s execution and pauses it at the specified line.
The debugger provides tools and a user interface that let you interact with the paused program. You can inspect variables, step through the code line by line, and better understand what your program is doing.
In this example, you’ll use VS Code to debug a function that capitalizes a list of fruits. It’s important to make sure you’re using a code editor that supports breakpoints.
First, define a function. In your code editor, create a file called fruit.py
, and add the following function definition:
fruit.py
def capitalize_fruit_names(fruits):
capitalized_fruit_names = []
for fruit in fruits:
capitalized_fruit_names.append(fruit.capitalize())
return capitalized_fruit_names
capitalize_fruit_names(["apple", "BANANA", 98, "Mango"])
Next, you’ll set a breakpoint. Locate the line inside your function where you want to inspect the execution—in this case, inside the for
loop.
In your editor, click on the left margin next to the line number to set a breakpoint. A red dot will appear:

Notice the red dot next to line five above. Now you’re ready to start debugging. In VS Code, click the Run and Debug icon in the Activity Bar on the side of the window. When you do this, the debugging panel will appear and look similar to the following screenshot:

Use the arrow icons to step through your code line by line or to run the code until the next breakpoint without stepping into function calls. You can also click the triangular Play button to run the program without interruption.
If an error occurs, an exception popup will appear just below the faulty line in the code editor, highlighting where there’s an issue. Repeat the process, fix the code, and resume execution until you’re satisfied that the variables behave the way they should.
When using the breakpoint tool, you’ll notice that you get an error when an integer tries to complete the for
loop process. This should guide you to put a check in place to exclude non-string values from the list.
Modify the capitalize_fruit_names()
function so that it handles non-string inputs by replacing them with empty strings:
fruit.py
def capitalize_fruit_names(fruits):
capitalized_fruit_names = []
cleaned = [fruit if isinstance(fruit, str) else "" for fruit in fruits]
for fruit in cleaned:
capitalized_fruit_names.append(fruit.capitalize())
return capitalized_fruit_names
The capitalize_fruit_names()
function has been rewritten to check if fruit
is of type string, and if it isn’t, the non-string value is replaced with an empty string (""
). Now, run the debugger again with the breakpoints and observe the difference. The program no longer throws an error, and the variables fruit
and capitalized_fruit_names
behave as expected.
Using Python’s Built-in breakpoint()
Function
Python’s built-in breakpoint()
function is a convenient way to introduce debugging into your code. It was introduced in Python 3.7 and serves as an easy-to-use function. When Python encounters breakpoint()
in your code, it pauses execution and enters an interactive debugging environment.
By default, breakpoint()
uses Python’s built-in debugger, pdb
, but you can configure it to use other tools. To use breakpoint()
, simply add the function call to a new line in the code where you’d like to add the breakpoint. This will have a similar effect to clicking on the left margin in VS Code.
How to Debug Python Errors With Tests
Another effective way to debug common errors in Python is through testing.
Writing tests before you write your functions will help you catch errors early. Even if you already have an error, you can still write a test with controlled inputs to find where the problem originated when you don’t receive the expected output.
In the next sections, you’ll learn what testing means in the context of debugging and how to use it to fix errors in your code.
Understanding Debugging With Tests
Using testing for debugging means writing and running small, focused pieces of code that check whether your functions behave as expected. These tests act as automated checks that can alert you when something isn’t working right.
Tests don’t just help you find errors—they can also help prevent them. If you write your tests before your functions, they can guide how you write the code itself. This method of writing tests before the code that implements the tested functions is called Test Driven Development. Even without adopting full TDD, you can use tests to systematically reproduce bugs, which makes finding and fixing problems much easier.
Debugging With Tests
To see how you can debug with unit tests, create a file called test_fruit.py
. In the file, add some test cases for the function called capitalize_fruit_names()
. This takes in a list of fruit names and returns the list with the fruit names capitalized.
To handle cases where the function encounters a non-string element in the input list of fruits, you should skip the non-string input and only capitalize the strings. Here are the example tests:
test_fruit.py
import unittest
from fruit import capitalize_fruit_names
class TestFruit(unittest.TestCase):
def test_empty_list(self):
"""with empty list"""
self.assertEqual(capitalize_fruit_names([]), [])
def test_lowercase(self):
"""with lowercase strings"""
self.assertEqual(
capitalize_fruit_names(["apple", "banana", "cherry"]),
["Apple", "Banana", "Cherry"],
)
def test_uppercase(self):
"""with uppercase strings"""
self.assertEqual(
capitalize_fruit_names(["APPLE", "BANANA", "CHERRY"]),
["Apple", "Banana", "Cherry"],
)
def test_mixed_case(self):
"""with mixed case strings"""
self.assertEqual(
capitalize_fruit_names(["mAnGo", "grApE"]),
["Mango", "Grape"],
)
def test_non_string_element(self):
"""with a mix of integer and string elements"""
self.assertEqual(
capitalize_fruit_names([123, "banana"]),
["", "Banana"],
)
if __name__ == "__main__":
unittest.main()
Here, you created five test cases based on the values of the list sent into the capitalize_fruit_names()
function. Each test checks whether the function produces the expected output. When you run the tests, any failing case helps you identify what’s missing or incorrect in the function’s behavior.
As an experiment, revert the fruit.py
file back to the original function, without any checks or validation for non-string values:
fruit.py
def capitalize_fruit_names(fruits):
capitalized_fruit_names = []
for fruit in fruits:
capitalized_fruit_names.append(fruit.capitalize())
return capitalized_fruit_names
When you run the test file, you’ll notice that all but the test_non_string_element
test case passes. Here’s the error you’ll see for the failed test:
$ python -m unittest test_fruit.py
...E.
======================================================================
ERROR: test_non_string_element (test_fruit.TestFruit.test_non_string_element)
with a mix of integer and string elements
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_fruit.py", line 33, in test_non_string_element
capitalize_fruit_names([123, "banana"]),
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
File "fruit.py", line 5, in capitalize_fruit_names
capitalized_fruit_names.append(fruit.capitalize())
^^^^^^^^^^^^^^^^
AttributeError: 'int' object has no attribute 'capitalize'
----------------------------------------------------------------------
Ran 5 tests in 0.002s
FAILED (errors=1)
The error above is triggered by the test_non_string_element()
test. In the test, you expect the function to replace any non-string values in the fruits list with empty strings.
However, this hasn’t been handled yet within capitalize_fruit_names()
. Since you need the function to handle this replacement, add the following code to update the function so that non-string values are replaced with empty strings:
fruit.py
def capitalize_fruit_names(fruits):
capitalized_fruit_names = []
cleaned = [fruit if isinstance(fruit, str) else "" for fruit in fruits]
for fruit in cleaned:
capitalized_fruit_names.append(fruit.capitalize())
return capitalized_fruit_names
If you run the tests again, you should get the following result, with all tests running smoothly:
$ python -m unittest test.py
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s
OK
By adding the replacement condition to the fruit.py
file, the test case for non-string values is now handled, and all tests pass. This confirms that the capitalize_fruit_names()
function behaves as it should.
Conclusion
You’re now able to anticipate and quickly fix errors in your code, and you’re well on your way to becoming a top-class developer.
In this tutorial, you’ve learned how to:
- Get started with debugging in Python
- Decode and understand tracebacks
- Add logs within your code using
print()
- Use breakpoints and inspect variables in your code
- Proactively anticipate errors with tests
As with every other aspect of programming, consistent practice is key to becoming a skilled Python developer. May your debugging sessions be quick, insightful, and effective!
Get Your Code: Click here to download the free sample code that shows you how to debug common Python errors.
Frequently Asked Questions
Now that you have some experience with debugging in Python, you can use the questions and answers below to check your understanding and recap what you’ve learned.
These FAQs are related to the most important concepts you’ve covered in this tutorial. Click the Show/Hide toggle beside each question to reveal the answer.
Common techniques include using tracebacks, adding print()
calls, setting breakpoints, and writing tests to help identify and fix errors in your Python code.
You can use tracebacks to pinpoint where an error occurred in your code by examining the error message, which points to the specific line and the type of error.
You can use Python’s breakpoint()
function to pause code execution and enter an interactive debugging environment, allowing you to inspect variables and control the flow of your program.
You can use print()
to output the values of variables and expressions at different points in your code, helping you track their state and identify where errors occur.
Writing tests helps you catch errors early by verifying that your code produces the expected results, ensuring reliability and preventing future bugs.
Take the Quiz: Test your knowledge with our interactive “How to Debug Common Python Errors” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
How to Debug Common Python ErrorsTake this quiz to review core Python debugging techniques like reading tracebacks, using print(), and setting breakpoints to find and fix errors.