Find & Fix Code Bugs in Python: Debug With IDLE

Find & Fix Code Bugs in Python: Debug With IDLE

by David Amos Sep 09, 2020 basics python

Everyone makes mistakes—even seasoned professional developers! Python’s interactive interpreter, IDLE, is pretty good at catching mistakes like syntax errors and runtime errors, but there’s a third type of error that you may have already experienced. Logic errors occur when an otherwise valid program doesn’t do what was intended. Logic errors cause unexpected behaviors called bugs. Removing bugs is called debugging.

A debugger is a tool that helps you hunt down bugs and understand why they’re happening. Knowing how to find and fix bugs in your code is a skill that you’ll use for your entire coding career!

In this tutorial, you’ll:

  • Learn how to use IDLE’s Debug Control window
  • Practice debugging on a buggy function
  • Learn alternative methods for debugging your code

Use the Debug Control Window#

The main interface to IDLE’s debugger is the Debug Control window, or the Debug window for short. You can open the Debug window by selecting Debug→Debugger from the menu in the interactive window. Go ahead and open the Debug window.

Whenever the Debug window is open, the interactive window displays [DEBUG ON] next to the prompt to indicate that the debugger is open. Now open a new editor window and arrange the three windows on your screen so that you can see all of them simultaneously.

In this section, you’ll learn how the Debug window is organized, how to step through your code with the debugger one line at a time, and how to set breakpoints to help speed up the debugging process.

The Debug Control Window: An Overview#

To see how the debugger works, you can start by writing a simple program without any bugs. Type the following into the editor window:

 1for i in range(1, 4):
 2    j = i * 2
 3    print(f"i is {i} and j is {j}")

Save the file, then keep the Debug window open and press F5. You’ll notice that execution doesn’t get very far.

The Debug window will look like this:

Image of IDLE's Debug window

Notice that the Stack panel at the top of the window contains the following message:

> '__main__'.<module>(), line 1: for i in range(1, 4):

This tells you that line 1 (which contains the code for i in range(1, 4):) is about to be run but hasn’t started yet. The '__main__'.module() part of the message refers to the fact that you’re currently in the main section of the program, as opposed to being, for example, in a function definition before the main block of code has been reached.

Below the Stack panel is a Locals panel that lists some strange looking stuff like __annotations__, __builtins__, __doc__, and so on. These are internal system variables that you can ignore for now. As your program runs, you’ll see variables declared in the code displayed in this window so that you can keep track of their value.

There are five buttons located at the top left-hand corner of the Debug window: Go, Step, Over, Out, and Quit. These buttons control how the debugger moves through your code.

In the following sections, you’ll explore what each of these buttons does, starting with Step.

The Step Button#

Go ahead and click Step at the top left-hand corner of the Debug window. The Debug window changes a bit to look like this:

Python IDLE Step button

There are two differences to pay attention to here. First, the message in the Stack panel changes to the following:

> '__main__'.<module>(), line 2: j = i * 2:

At this point, line 1 of your code has run, and the debugger has stopped just before executing line 2.

The second change to notice is the new variable i that is assigned the value 1 in the Locals panel. That’s because the for loop in the first line of code created the variable i and assigned it the value 1.

Continue hitting the Step button to walk through your code line by line, and watch what happens in the debugger window. When you arrive at the line print(f"i is {i} and j is {j}"), you can see the output displayed in the interactive window one piece at a time.

More importantly, you can track the growing values of i and j as you step through the for loop. You can probably imagine how beneficial this feature is when trying to locate the source of bugs in your programs. Knowing each variable’s value at each line of code can help you pinpoint where things go wrong.

Breakpoints and the Go Button#

Often, you may know that the bug must be in a particular section of your code, but you may not know precisely where. Rather than clicking the Step button all day long, you can set a breakpoint that tells the debugger to continuously run all code until it reaches the breakpoint.

Breakpoints tell the debugger when to pause code execution so that you can take a look at the current state of the program. They don’t actually break anything.

To set a breakpoint, right-click (Ctrl-click on a Mac) the line of code in your editor window that you would like to pause at and select Set Breakpoint. IDLE highlights the line in yellow to indicate that your breakpoint has been set. To remove a breakpoint, right-click the line with the breakpoint and select Clear Breakpoint.

Go ahead and press Quit at the top of the Debug window to turn off the debugger for now. This won’t close the window, and you’ll want to keep it open because you’ll be using it again in just a moment.

Set a breakpoint on the line of code with the print() statement. The editor window should now look like this:

Python IDLE breakpoint pt. 1

Save and run the file. Just like before, the Stack panel of the Debug window indicates that the debugger has started and is waiting to execute line 1. Click Go and watch what happens in the Debug window:

Python IDLE Go button pt. 1

The Stack panel now shows the following message indicating that it’s waiting to execute line 3:

> '__main__'.<module>(), line 3: print(f"i is {i} and j is {j}")

If you look at the Locals panel, then you’ll see that both variables i and j have the values 1 and 2, respectively. By clicking Go, you told the debugger to run your code continuously until it reached either a breakpoint or the end of the program. Press Go again. The Debug window now looks like this:

Python IDLE Go button pt. 2

Do you see what changed? The same message as before is displayed in the Stack panel, indicating that the debugger is waiting to execute line 3 again. However, the values of the variables i and j are now 2 and 4. The interactive window also displays the output from having run the line with print() the first time through the loop.

Each time you press the Go button, the debugger runs the code continuously until it reaches the next breakpoint. Since you set the breakpoint on line 3, which is inside the for loop, the debugger stops on this line each time it goes through the loop.

Press Go a third time. Now i and j have the values 3 and 6. What do you think will happen when you press Go one more time? Since the for loop only iterates three times, when you press Go again, the program will finish running.

Over and Out#

The Over button works sort of like a combination of Step and Go. It steps over a function or a loop. In other words, if you’re about to step into a function with the debugger, then you can still run that function’s code without having to step all the way through each line of it. The Over button takes you directly to the result of running that function.

Likewise, if you’re already inside a function or loop, then the Out button executes the remaining code inside the function or loop body and then pauses.

In the next section, you’ll look at some buggy code and learn how to fix it with IDLE.

Squash Some Bugs#

Now that you’ve gotten comfortable with using the Debug Control window, let’s take a look at a buggy program.

The following code defines a function add_underscores() that takes a single string object word as an argument and returns a new string containing a copy of word with each character surrounded by underscores. For example, add_underscores("python") should return "_p_y_t_h_o_n_".

Here’s the buggy code:

def add_underscores(word):
    new_word = "_"
    for i in range(len(word)):
        new_word = word[i] + "_"
    return new_word

phrase = "hello"
print(add_underscores(phrase))

Type this code into the editor window, then save the file and press F5 to run the program. The expected output is _h_e_l_l_o_, but instead all you see is o_, or the letter "o" followed by a single underscore.

If you already see what the problem with the code is, don’t just fix it. The point of this section is to learn how to use IDLE’s debugger to identify the problem.

If you don’t see what the problem is, don’t worry! By the end of this section, you’ll have found it and will be able to identify similar problems in other code you encounter.

Debugging is problem solving, and as you become more experienced, you’ll develop your own approaches. In this section, you’ll learn a simple four-step method to help get you started:

  1. Guess which section of code may contain the bug.
  2. Set a breakpoint and inspect the code by stepping through the buggy section one line at a time, keeping track of important variables along the way.
  3. Identify the line of code, if any, with the error and make a change to solve the problem.
  4. Repeat steps 1–3 as needed until the code works as expected.

Step 1: Make a Guess About Where the Bug Is Located#

The first step is to identify the section of code that likely contains the bug. You may not be able to identify exactly where the bug is at first, but you can usually make a reasonable guess about which section of your code has an error.

Notice that the program is split into two distinct sections: a function definition (where add_underscores() is defined), and a main code block that defines a variable phrase with the value "hello" and then prints the result of calling add_underscores(phrase).

Look at the main section:

phrase = "hello"
print(add_underscores(phrase))

Do you think the problem could be here? It doesn’t look like it, right? Everything about those two lines of code looks good. So, the problem must be in the function definition:

def add_underscores(word):
    new_word = "_"
    for i in range(len(word)):
        new_word = word[i] + "_"
    return new_word

The first line of code inside the function creates a variable new_word with the value "_". You’re all good there, so you can conclude that the problem is somewhere in the body of the for loop.

Step 2: Set a Breakpoint and Inspect the Code#

Now that you’ve identified where the bug must be, set a breakpoint at the start of the for loop so that you can trace out exactly what’s happening inside the code with the Debug window:

Python IDLE breakpoint pt. 2

Open the Debug window and run the file. Execution still pauses on the very first line it sees, which is the function definition.

Press Go to run through the code until the breakpoint is encountered. The Debug window will now look like this:

Python IDLE Debug window pt. 1

At this point, the code is paused just before entering the for loop in the add_underscores() function. Notice that two local variables, word and new_word, are displayed in the Locals panel. Currently, word has the value "hello" and new_word has the value "_" as expected.

Click Step once to enter the for loop. The Debug window changes, and a new variable i with the value 0 is displayed in the Locals panel:

Python IDLE Debug window pt. 2

i is the counter used in the for loop, and you can use it to keep track of which iteration of the for loop you’re currently looking at.

Click Step one more time. If you look at the Locals panel, then you’ll see that the variable new_word has taken on the value "h_":

Python IDLE Debug window pt. 3

This isn’t right. Originally, new_word had the value "_", and on the second iteration of the for loop it should now have the value "_h_". If you click Step a few more times, then you’ll see that new_word gets set to e_, then l_, and so on.

Step 3: Identify the Error and Attempt to Fix It#

The conclusion you can make at this point is that, at each iteration of the for loop, new_word is overwritten with the next character in the string "hello" and a trailing underscore. Since there’s only one line of code inside the for loop, you know that the problem must be with the following code:

new_word = word[i] + "_"

Look at the line closely. It tells Python to get the next character of word, tack an underscore onto the end of it, and assign this new string to the variable new_word. This is exactly the behavior you’ve witnessed by stepping through the for loop!

To fix the problem, you need to tell Python to concatenate the string word[i] + "_" to the existing value of new_word. Press Quit in the Debug window, but don’t close the window just yet. Open the editor window and change the line inside the for loop to the following:

new_word = new_word + word[i] + "_"

Step 4: Repeat Steps 1 to 3 Until the Bug Is Gone#

Save the new changes to the program and run it again. In the Debug window, press Go to execute the code up to the breakpoint.

The program pauses just before entering the for loop in add_underscores(). Press Step repeatedly and watch what happens to the new_word variable at each iteration. Success! Everything works as expected!

Your first attempt at fixing the bug worked, so you don’t need to repeat steps 1–3 anymore. This won’t always be the case. Sometimes you’ll have to repeat the process several times before you fix a bug.

Alternative Ways to Find Bugs#

Using a debugger can be tricky and time consuming, but it’s the most reliable way to find bugs in your code. Debuggers aren’t always available, though. Systems with limited resources, such as small Internet of Things devices, often won’t have built-in debuggers.

In situations like these, you can use print debugging to find bugs in your code. Print debugging uses print() to display text in the console that indicates where the program is executing and what the state of the program’s variables are at certain points in the code.

For example, instead of debugging the previous program with the Debug window, you could add the following line to the end of the for loop in add_underscores():

print(f"i = {i}; new_word = {new_word}")

The altered code would then look like this:

def add_underscores(word):
    new_word = "_"
    for i in range(len(word)):
        new_word = word[i] + "_"
        print(f"i = {i}; new_word = {new_word}")
    return new_word

phrase = "hello"
print(add_underscores(phrase))

When you run the file, the interactive window displays the following output:

i = 0; new_word = h_
i = 1; new_word = e_
i = 2; new_word = l_
i = 3; new_word = l_
i = 4; new_word = o_
o_

This shows you what the value of new_word is at each iteration of the for loop. The final line containing just a single underscore is the result of running print(add_underscore(phrase)) at the end of the program.

By looking at the above output, you can come to the same conclusion you did while debugging with the Debug window. The problem is that new_word is overwritten at each iteration.

Print debugging works, but it has several disadvantages over debugging with a debugger. First, you have to run your entire program each time you want to inspect the values of your variables. This can be an enormous waste of time compared to using breakpoints. You also have to remember to remove those print() function calls from your code when you’re done debugging!

The example loop in this section may be a good example for illustrating the process of debugging, but it’s not the best example of Pythonic code. The use of the index i is a giveaway that there might be a better way to write the loop.

One way to improve this loop is to iterate over the characters in word directly. Here’s one way to do that:

def add_underscores(word):
    new_word = "_"
    for letter in word:
        new_word = new_word + letter + "_"
    return new_word

The process of rewriting existing code to be cleaner, easier to read and understand, or more in line with the standards set by a team is called refactoring. We won’t discuss refactoring in this tutorial, but it’s an essential part of writing professional-quality code.

Conclusion: Python Debugging With IDLE#

That’s it! You now know all about debugging using IDLE’s Debug window. You can use the basic principles you used here with a number of different debugging tools. You’re now well equipped to start debugging your Python code.

In this tutorial, you learned:

  • How to use IDLE’s Debug Control window to inspect the values of variables
  • How to insert breakpoints to take a closer look at how your code works
  • How to use the Step, Go, Over, and Out buttons to track down bugs line by line

You also got some practice debugging a faulty function using a four-step process for identifying and removing bugs:

  1. Guess where the bug is located.
  2. Set a breakpoint and inspect the code.
  3. Identify the error and attempt to fix it.
  4. Repeat steps 1 to 3 until the error is fixed.

Debugging is as much an art as it is a science. The only way to master debugging is to get a lot of practice with it! One way to get some practice is to open the Debug Control window and use it to step through your code as you work on the exercises and challenges you find in other Real Python tutorials.

For more information on debugging Python code, check out Python Debugging With Pdb. If you enjoyed what you learned in this sample from Python Basics: A Practical Introduction to Python 3, then be sure to check out the rest of the book.

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About David Amos

David Amos David Amos

David is a mathematician by training, a data scientist/Python developer by profession, and a coffee junkie by choice.

» More about David

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Join us and get access to hundreds of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Join us and get access to hundreds of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Real Python Comment Policy: The most useful comments are those written with the goal of learning from or helping out other readers—after reading the whole article and all the earlier comments. Complaints and insults generally won’t make the cut here.

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Keep Learning

Related Tutorial Categories: basics python