Tips and tools for software debugging
What do we face when we find a bug? What are useful techniques or tools for debugging our app? What to do when dealing with a rather difficult bug? In this post I want to introduce you to one particular tool, its potential, and how much it can help in the debugging process.
Building software is like building a house
To build a house you need professionals: the architect who designs and plans what needs to be done, the workers who build it, and the supervisors who check that everything is OK. A software project has similar roles and we designers and developers fulfill these same objectives at different stages or moments in the software development process.
At the beginning, we are like the architect during the design and planning process. We build the product roadmap, ideate, prototype the solution, and validate it with users and the business. At the development stage, we are like the workers who build the features. And then we play the role of the supervisor when we test the app, and when we present demos to the client, among other things.
But like any project, things break or unexpected things happen. We have different ways to troubleshoot in our application.
One of the most usual ways is user feedback, wherein the people who use the system tell us when something is not working. We also have the QA process, where fellow testers review the code and features and, if something is wrong or broken, they report it to us.
In short, we have different systems that can alert us to problems with our product. We may also notice a failure or error ourselves, since we are in constant contact with our platform. And it is normal to find things that should not be there or should not be that way because at the beginning, when we envisioned the project, we may have imagined something different.
It’s alright to make mistakes. Once we recognize that we are human and that these things can and will happen, that’s when we can face the inevitability of bugs – it’s part of our job. Of course, bugs generate frustration, but the important thing is to have a solution-oriented attitude, to understand that having bugs is part of the process, and to ask ourselves, how can I solve them? Let’s dive deeper.
Many of the bugs we face when programming in Python come from very common errors or exceptions. One of the most common exceptions is KeyError, which occurs when we access a key in a dictionary and it does not exist. TypeError is also very common, when an operation or function is applied to an object of the wrong type. And finally, ValueError. This exception occurs when an operation or function receives an argument with an invalid value. Many times, the name of the exception is the only clue we have to discover the bug’s cause, and with this information we can start the debugging process.
First of all, what is a bug?
A bug is an error or flaw in a software program that causes an unexpected result or makes the software behave incorrectly. A bug can arise from errors in how we write source code or how we interpret user requirements. It can be small and almost undetectable, such as a text not being where it should be, or more extreme, such as the program crashing completely.
Fun fact: it is rumored that the term “bug” was coined because they found a moth trapped in a computer and it was causing an error. According to Wikipedia, in 1946, when Hopper was released from active duty, she joined the Harvard Faculty at the Computation Laboratory where she continued her work on the Mark II and Mark III. Operators traced an error in the Mark II to a moth trapped in a relay, coining the term “bug.”
And what is debugging?
Debugging is the process of eradicating a bug. There is a definition of debugging that explains it as: “Being the detective of a crime in which we are also the murderer.”
Debugging is not easy; there are many strategies and tools that can help make our lives easier in finding and fixing these bugs. 90% of the process of solving a bug is to be able to reproduce it. This is our entry point to the bug because it shows us where to start looking.
Once we are able to reproduce the bug, we record the steps to reproduce it and put the expected result. It is important that we leave the steps to follow recorded, along with the expected result, the actual result, and many screenshots or videos to prove it. This is very useful for developers as it makes it much easier to understand. Once it is fixed, we follow the same steps we did to reproduce it until we see that it does not happen anymore.
Another important thing about reproducing bugs is that sometimes they are intermittent (a developer’s worst nightmare) or there are bugs that depend on some random factor, such as bugs that only occur at a certain time or day. That does not allow us to reproduce it whenever we want, which makes the process more complex.
What happens if we don’t reproduce it? If we don’t reproduce it, it will be very difficult to know if the problem is truly fixed.
What techniques and tools can we use to reproduce the bugs?
The traceback is the log that appears in the console when an exception occurs and shows us at the top what the error was and where it occurred. It is used to review a code flow that was executed, file by file, line by line, until the error occurred. The traceback is useful up to a certain point because without context where the error occurred, we will not be able to solve it. That’s why there are some other tools that show us more information.
Sentry is an example that shows us the context and the value of the different variables where the exception occurred. This tool gives us more clues to solve the crime. It allows us to catch errors as they happen because it sends an alert so we can see the error in real time.
Rubber Duck Debugging
In this technique the idea is to put a rubber duck next to your computer and you must explain to it what your code does, line by line, as if it were someone who has no prior knowledge of the subject. The goal is to notice contradictions or errors.
This technique helps the developer to remain calm. Many times when we spend hours fixing a bug we end up getting cloudy, that’s why it allows you to bring the concepts down to earth. Other tools analogous to this one are: go for a walk and come back with fresh ideas, talk to someone else, ask for help. These can help trigger ideas to find more clues and solve the bug.
They are the most used option when solving bugs since they allow us to know the system’s state and variables and understand where the code goes, which is the flow. How to do it? We fill everything with prints or console logs and we reproduce the bug. There we can see the variables and the state of the system when the bug arose.
After using one or more of these techniques, we generally find the bug and can fix it. However, in the case of more complex bugs, it is useful to use a debugger.
Understanding the debugging
A debugger is a process that aims to execute another process in a controlled way. It usually works as a wrapper over the real process that we want to execute. It mainly allows us to examine the state of a running program. With the console logs we can see the variable value once the program is executed and the debugger allows us to do that while the program is running. When we make a print or a console.log in the code to know the value of a variable, this is printed in the console and we see it once the program is finished executing. Unlike the print/console.log, with a debugger we can pause the program execution at a certain point and see the state of our variables.
In the debugger the idea is to put breakpoints in the code where we want the execution to pause. So, we put different breakpoints where we expect the code to pass and where it cuts with that breakpoint, the execution is paused and there we can observe the state of the system.
There are two types of debugger:
- CLI: pdb or ipdb, debuggers that work through the command line. Also, pdb is included inside Python.
- GRAPHICS: included in the IDE, such as Visual Studio Code or Pycharm.
The difference between these two types of debugger is how we interact with it. One is through the command line, being able to move with commands in the console. The other works with a graphical interface and we can interact with different buttons and sections in our IDE.
To use the debugger, the first thing we have to do is place a breakpoint where we want to pause the execution. To do this, we import pdb and call the set_trace() function where we want the breakpoint to be.
cars = Car.objects.all()
import pdb; pdb.set_trace()
for car in cars:
As of Python 3.7, a more intuitive way can be used with the breakpoint function.
cars = Car.objects.all()
for car in cars:
We run our Django server, and execute that line of code. The console should change and we start using the debugger.
In this new shell we have several commands that we can use.
list: shows us where we are standing in the code. The arrow indicates which line is about to be executed.
next: allows us to advance the execution to the next line of code.
step: allows us to get into the method we are calling, in this case get_speed().
continue: continues execution until the next breakpoint.
In the shell we can inspect the variables’ values by writing them when they have already been initialized.
How to set up a Django Debugger in Visual Studio Code
First, we need to create a launch.json file that tells Visual Studio Code how to run the debugger with Django. To do this, we go to the debug section of vs code and create a configuration for Django.
To add a breakpoint you can click on the bar next to the line numbers. There will be a red circle where the breakpoint is set. Then in the debug section, we execute the debugger configuration we created previously.
This runs the Django server and we can interact with our application. Once execution stops, we can use the same debugger commands from the vs code GUI.
We can inspect variables, advance line by line and get into method calls. On the left, we can see the variables and their value in the context where the code is running. The watch section is a utility that we can use to set variables that we want to be watching all the time or even set expressions.
The call stack allows us to see the different parts where the code passes until it reaches the breakpoint or where we are inspecting. It shows us all the previous method calls.
Finally, the Debug Console. It works as a Python shell that allows us to interact with the variables and the context where we are executing. It also allows us to change it to generate different behaviors.
When to use the debugger?
- When you are handling very complex code with many branches or many parts where the code can be executed, usually because there are many “ifs” that can lead the execution to one side or the other.
- Many returns, that is, many places where the code can finish executing.
- Very long method or file.
- Don’t know where to put the prints.
- Complex calculations.
- When there is little knowledge of code, when we enter a new project and we are faced with a new section of code, the debugger shows us how the code works, the flows, and what the different use cases are.
When not to use the debugger?
- For a proof of concept it’s faster to add a print or console.log. Many times we suspect where the bug is, so trusting our intuition and using a print can save us the time it takes to raise the debugger.
- When the debugger is not yet configured. The configuration example we showed using Django is easy, but there are other contexts that can be much more difficult and/or expensive to configure a debugger and it is not worth doing when we are in the process of fixing a bug.
Ideally, when starting a project, a debugger should already be configured for the developers. Otherwise, dedicate some time in a sprint to set it up, because in the long term it will benefit the team to see in real time when things happen and their flows.
Again, the debugger is just a tool, it will not prevent bugs. But it’s a nice tool for improving the code making process.
If you know of any other interesting tools to prevent or fix bugs, be sure to share it with us on our social networks.
And if you are interested in learning about other tools we use at Octobot for software development, we invite you to read this blog post about Git and the world behind it.
Also, read another blog post written by myself about debugging here.