This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

05. Debugging

Debugging strategy and the basic features of the Visual Studio Code debugger.

You are getting the first edition of all these pages. Please let me know if you find an error!

1 - Terms and concepts

Vocabulary you need to know plus what debugging really is.

You are getting the first edition of all these pages. Please let me know if you find an error!

What is debugging?

Debugging is the process of comprehending how a program arrived at a particular state.

Errors are incorrect calculations or bad states of a program. An error occurs while the program is running. Errors show as bad output, crashes, and the like. Debugging is often about comprehending how you arrived at an error.

Defects are programming mistakes, logic flaws, or problems with design that could lead to errors. What did you do wrong? Defects are problems or mistakes, errors are the tangible result of running a program with a defect.

Colloquially, we conflate these two terms into the concept of a “bugs”, and hence the term “debugging”.“Bug” is an old term pre-dating computers, but Admiral Grace Hopper, who is largely responsible for us no longer programming in Assembly Language, popularized the term “bug” in computing after she found one in the Harvard Mark II computer:

A bug found in the Harvard Mark II computer

What is program state?

You have no doubt used print() statements to understand your program. Printing variables, or printing here to see if a line executes is common. You are debugging using print statements.

Think about what these print statements tell you. They tell you:

  1. What are the variable values at a point in time?
  2. Which lines of code are getting executed when?

These two pieces of information are the essence of debugging. Let’s formalize them:

  1. step: the program statement (often a single line of code) that was just executed.
  2. state of a program is comprised of:
    • the variable values at the step.
    • the call stack at the step. We will explain this in a moment.

Again, debugging is trying to understand how you arrived at a particular state (incorrect calculation, a crash).

Debugging from an exception

Let’s examine some debugging info. Do the following exercise to get set up.

Setup

  1. Use the Terminal to create a directory called debugging-lab/ in the same place you are gathering all your code for this class.
  2. Download bad_math.py and save it to the debugging-lab/ directory.
  3. cd into the debugging-lab/ directory and run code . to start Visual Studio Code in that directory.
  4. Select the bad_math.py file, then Run it WITHOUT DEBUGGING, either:
    • Go to the menu at the top and do Run > Run without Debugging
    • Right click in the editor and select Run Python > Run Python File in Terminal
  5. The program should crash with an error.

If the program crashes due to an exception, the stack trace will usually point you to the line of code that exploded: an exception stack trace showing the error line

There is a lot of useful information in this stack trace to start the debugging process.

It tells you that the error is in bad_math.py, line 4 and even shows you the offending line of code.

Don’t fix any bugs yet. We want them for the next lab.

The error is an IndexError: list index out of range. So the program tried to execute numbers[i] but likely i was too big.

The other lines show the call stack, or the chain of function calls that are active in memory. In Python,the top-most function was called first, and the bottom-most function was called last (it is the reverse in Java):

  1. Line 30 of <module> called the main() function. - <module> represents the file bad_math.py itself and any code in the file that is not in a function or class.
  2. Inside main() on line 18, largest_number = find_largest(numbers) was called.
  3. Finally, inside find_largest(), the buggy line was called that generated the exception and crashed the program.

So the call stack is the chain of active functions that are waiting for something to be computed and returned. <module> -> main() -> find_largest(), which errored out. Look at the code itself to confirm the chain of function calls.

Congratulations! You have found some essential debugging information: the step at which the error occurred and the call stack portion of the state. What key debugging information are you missing?

The variable values! Now go to line 4. Add print(i) and print(numbers) right before that line to see what values i and numbers when the crash happens. That should give you a strong hint on what happened and how to fix it.

Don’t fix any bugs yet. We want them for the next lab.

Debugging is a process

A good software engineer follows a structured process. Use the exception message or your knowledge of the program to say, “Well, the problem could be this.” Form a hypothesis. Then add print statements to help determine state around the problematic step. Try different input values to confirm your hypothesis.

Maybe you will discover your hypothesis is incorrect. No problem! Maybe the error is actually due to something earlier in the call stack. Move your print statements up the stack and try again.

Whatever you do, build and refine your hypotheses. Do not just try something to see if it works. You may get lucky and fix the problem, but if you don’t understand the fix, how do you really know? You will also be doomed to make the same mistake again if you don’t understand what happened.

A better way?

You can debug just fine with print statements, but managing them is tedious. You will also have times where it would be useful to pause execution of the program at a certain point say, on the first iteration of a loop.

You can get state with print and control steps with code, but modern debugging tools will simplify this process while keeping your code clean.

We illustrate how to use Visual Studio Code’s debugger in the next lab.

Knowledge check

  • Question: What two elements comprise the state of a program at a particular step?
  • Question: Suppose you use a constant value that never changes in your program, like pi = 3.14159. Do you think the variable pi is part of the program state? Why or why not?
  • Question: When do you see a stack trace? What information does it contain?
  • Question: Explain the difference between an error and a defect. Give an example of a defect and its resulting error.
  • Question: What information about the running program is contained in the call stack?

2 - The Visual Studio Code debugger

Use the power of the IDE to understand your code.

You are getting the first edition of all these pages. Please let me know if you find an error!

Debugging support tools have been around since the 70s. All modern IDEs, like Visual Studio Code, let you control the steps of program execution while showing the program state. Debugging tools, properly used, are much more efficient than print statements.

Running the debugger

If you didn’t do it in the Debugging Basics lab, create a debugging-lab/ directory and download bad_math.py to it.

  1. Open the debugging-lab/ directory and open bad_math.py in an editor.
  2. Run the program in debug mode by doing one of:
    • Hit your F5 key.
    • Select Run > Start Debugging from the top menu.
    • Click the Debug pane on the left sidebar, then the Run & Debug button. Debug pane entry in Code
  3. The first time you debug a file you will need to choose a debugger. Choose the Python Debugger suggested by Code. selecting a debugger
  4. You will now be prompted to select a debugging configuration. Choose Python File: Debug the currently active Python file. selecting a debugging configuration

The Visual Studio Code debugger should now launch. Notice that you are now in the Debugging pane of Visual Studio Code, which is accessible anytime from the left sidebar. This pane will open any time you Run a program with debugging.

You should see something similar to the following: Visual Studio Code debugger screen with exception The bad_math.py program should crash with an exception. Here are the essential elements you see:

  1. The editor shows the exception details in a red box. The yellow line and arrow mark the step the program was on when it crashed.
  2. These are the step controls. Visual Studio Code automatically paused on the step that caused the crash. More on the controls below.
  3. The variable pane shows the values of all variables in scope at the current step. Variable values are one part of the program state.
  4. The watch pane lets you isolate variables you want to monitor. Similar to the variable pane.
  5. The call stack is the other part of the program state. It shows the stack of function calls that arrived at the current step.

Using the step controls, hit either the blue “play” icon or the red “stop” icon. Stop will cancel execution and produce nothing, play will continue execution of the program, resulting in the exception printing in the Terminal (where the program is running) and the program will crash.

Breakpoints and stepping

The Visual Studio Code debugger will automatically break (pause) execution on steps that throw an exception. You can look at the variable pane and call stack to understand the state of the program and hopefully gain insight into what happened.

However, you will often want to break execution at step of your choosing, not just when an exception happens. Maybe want to see how a value was computed and what the variables were well before the crash happened. Or maybe your program doesn’t crash at all, but simply produces the wrong output.

You add breakpoints in the IDE to tell the debugger on which step(s) to pause execution. To set a breakpoint:

  1. Left click in the blank space to the left of the line number in the code editor. A red dot will appear to indicate the breakpoint. Set a breakpoint on line 3.
    • Click the breakpoint again to remove it.
    • You can set multiple break points.
    • You cannot set a breakpoint on a blank line of code.
  2. Run the program with Debugging from the Run menu or hit F5.
  3. The debugger will break (pause execution) on line 3 or on whichever line you placed the breakpoint. debugger pane showing an initial breakpoint
  4. Use the step controls to control the execution of the program. All of these controls have a keyboard shortcut as well.
    • continue button - continue execution until the next breakpoint or the program ends.
    • step over button - Step Over the current line, which means evaluate the line and go to the next one.
    • step into button - Step Into the current line. Super the current line calls a function like if my_fun(x) == True, the debugger will step into the my_fun() function and step through it. If you did step over, the debugger would evaluate the entire line including the my_fun() call without pausing.
    • step out - Step Out of the current function. This will immediately complete all lines of the current function and pause at the line that called the current function in the call stack.
    • restart the debugger - Restart the debugging on the program. Just like re-running it. All your breakpoints will be retained.
    • stop the debugger - Stop the debugger without further execution of the code.

Notice that the variable pane, watch pane, and call stack update with each step. So now, using breakpoints and the step controls, you can precisely control the execution of the program to more methodically track down what is going on.

Adding a watch variable

The variables pane shows all variables in scope at each step. This set of variables can be overwhelming, and you often won’t care about most of the variables.

The watch pane lets you specify variables you want to watch specifically. To set a watch variable:

  1. Set a breakpoint and start debugging the program
  2. Either:
    • Select the variable in the editor, then Right Click > Add to Watch; or
    • In the watch pane, click the + to Add Expression and type in the name of the variable, e.g., type the name largest. adding a variable to the watch list

Now you will see your watched variables update as you step through the program. You can add as many watch variables as you like.

Conditional breakpoints

Using the watch pane helps you focus on what’s important as you refine your “what’s going on here?” hypothesis while debugging.

You will also find it useful to only have a breakpoint trigger under certain conditions. For example, you are reading file full of 10,000 hospital patient records and you figure out that the program crashes when it gets to the record belonging to “Alice St. John”. Unfortunately, Alice is record 342. You don’t want to set a breakpoint on the offending line and have to hit the Continue control 341 times to figure out what’s going on with Alice’s data.

Enter the conditional breakpoint, which is a breakpoint that only pauses execution when an expression you specify evaluates to True. Try it with our bad_math.py sample:

  1. Right click to the left of Line 3 and select Add Conditional Breakpoint
  2. A textbox will appear with Expression on the left. Type largest == 12 in the textbox.
  3. Now hit the Continue control. The conditional breakpoint will only pause when largest == 12.

Conditional breakpoints are extremely useful for refining your hypothesis as to what’s going on. Note you can enter any Python expression that evaluates to True or False, for example:

  • largest == 12 and i < 8
  • largest >= 5

Starting with vs. without debugging

When running your program, you have the option to Start with Debugging or Start without Debugging. What’s the practical difference?

Starting without debugging will not pause on breakpoints or exception, nor will variable values be tracked. Running without debugging will not affect any breakpoints or watch variables you have set – it just doesn’t update them.

Starting with debugging will do everything we showed, but significantly slows down the execution time of your program.

Exercise

There are 4 bugs present in the initial bad_math.py that can be triggered based on which value the numbers variable has. The various calls to main() at the bottom of the file are sufficient to reveal all the bugs.

Find and remove them. There are multiple ways to squash the bugs. You may squash two bugs at once depending on how you fix the first bug that causes the exception we have seen in our examples.

Knowledge check

  • Question: How do you start running a program in debug mode in Visual Studio Code?
  • Question: How do you add a variable to the watch list from the editor view?
  • Question: How do you set a conditional breakpoint that pauses when x evaluates to False?
  • Question: What is the difference between Step Over and Step Into in terms of the next step of execution?

Additional resources

3 - More practice

Additional samples for you to practice on.

You are getting the first edition of all these pages. Please let me know if you find an error!

Use these files to practice your debugging skills with the Visual Studio Code debugger. Look for the keyword BUG in the files on how to expose the error.

  1. fibonacci.py
  2. discount.py
  3. inventory.py