A software engineer, data scientist, web developer (or anyone really who doesn’t work with their hands) is only as good as the tools at his or her disposal. And for someone who needs to write and fix computer code, the debugger is one of the most important tools. It, therefore, surprises me to meet programmers, albeit usually novices, that yet have to discover the power of a debugger.
Usually, when someone has just started learning to program, they would like to see the value of variables in different places of their program. The most straightforward way to do this, and often recommended at the beginning of programming courses, is to insert “print” or “log” statements into the code. Once they learn this so-called print statement debugging, they enthusiastically start littering their code with these to help them understand how their code is running.
Using “print” statements is certainly a simple and viable method to visualize the state of your program. Even more, it is the only way in systems where you only have access to the program output. However, every time you add a “print” statement to your program, you need to recompile it (for compiled programming languages) and run it again. Which makes this trial-and-error process quite a time-consuming task.
There are also more interactive ways to write code, which give you additional feedback while you develop. Some developers like to work with read-eval-print loop (REPL in short) tools. Jupyter notebooks is one such tool that is popular with data scientists.
REPL-based tools evaluate short code snippets, provided by the developer, and return the result immediately. Each evaluated piece of code also affects the current REPL state. New variables can be defined or existing ones modified. This lets the developer run a program bit by bit and get immediate feedback on the result of their code.
REPL tools are a good alternative to print statement debugging. They make it easy to tweak the program state or the bit you would like to test and see the result. Their challenge ultimately is recreating the state of your program. In order to do this, one has to first import or recreate all the required variables and functions. At this point some developers may be thinking “but is there a better way?”. “Yes” is the answer. Get ready to enter a new chapter of your programming career.
A debugger is a computer program that runs your code interactively. It lets you define conditions in which your program should be interrupted. Once interrupted, you can inspect the program state at that point. Or you can run your program line-by-line to see how the state changes while your program is running. These features boost your productivity as a programmer significantly, because they let you single out where in your code a bug originates.
But not only that. A debugger can also assist you with understanding your own code, your colleague’s code, and even code from imported libraries and frameworks. It is not rare for me to step into the code of my project’s dependencies to understand why it’s behaving a certain way. It has even helped me contributing to open-source projects by exactly pinpointing which lines need to be corrected to fix a bug.
Dev’s spend about 50% of their time making their code work and fixing bugs. Using a debugger can speed up this process greatly, so developers can spend more time on work that actually provides value: adding new features.
There are two types of debuggers: source-level debuggers and machine-language debuggers. We’ll be focusing on source-level debuggers, as the chance that you will ever need to debug machine language (i.e. assembly code) is minimal, unless you work on compilers or need to optimize low-level routines.
Another axis on which debugging tools can be compared is whether they offer a command-line interface (CLI) or a graphical user interface (GUI) - some offer both. CLI debuggers can be useful when there isn’t a graphical environment available, so if you’re developing on a machine through SSH for example. But for all the other cases I recommend using a GUI debugger. Or even better, a debugger integrated into your IDE. These graphical debuggers have a much gentler learning curve. And usually, they display more information with fewer required keystrokes than a CLI debugger.
How to use a debugger
Here I will discuss some of the concepts commonly found in debuggers and how they can help you become more productive.
My favorite debugger at the moment is the one integrated into JetBrains IDE’s [I am not affiliated to them whatsoever]. So for the rest of the article, I will apply the same terminology that they use. But don’t click away yet! Other debuggers function similarly and apply the same concepts. So even if you have a preference for an IDE like Visual Studio Code, Eclipse, or NetBeans, this will still be useful to you.
To start debugging, run your code in debug mode. This can usually be done by clicking a button with an icon of a beetle or with just the text “Debug”. I would advise you to look up the keyboard shortcut for this mode as early as possible, as you’ll probably going to need it a lot!
With debug mode enabled, debuggers will run your code while keeping track of the current variables and their values. When you suspend the debugger (you will learn how later), you will be able to inspect all of the variables. In JetBrains IDE’s you can also see the current variable value by clicking right from the place where variables are defined.
Something else you can usually view are the stacked frames of your program. A frame is essentially the current scope of your program. If you execute a function, a new frame is created. For multi-threaded applications, it can be useful to navigate the various threads as well. Each thread has its own set of frames and respective variable states.
While your program is suspended, you can also evaluate complex expressions. Simply submit an expression in the current programming language and press “Evaluate”. This lets you drill down to the bottom of every variable. You can check the contents of a list or specific items in a JSON object. If you find yourself evaluating the same expression often, consider adding it as a watch. This will add the expression to the tracked variables so that you can see the result at any point.
When you start your program in debug mode, it will run continuously until it has completed or hit an exception. In order to start inspecting the program state, you need to set breakpoints. These are conditions, that when met, suspend program execution. At that point, you get access to all variables in scope, the option to execute expressions and control further execution. Even wilder is being able to edit the variable value during run-time and see how that affects your program.
There are different types of breakpoints. You can break at a specific line in the code. This is usually done by clicking in the gutter left of the line, or by hitting a certain keyboard combination when your caret is on the line. In addition, one can add a condition that must be met for the program to pause at that breakpoint.
Another possibility is to break when an exception is raised. This very useful to find out in which case a bug arises. In JetBrains IDE’s you first need to open the menu that shows you all breakpoints. By default, the option will be shown for breaking on any exception. To break on a specific exception, click the “+” button.
Some debuggers have the ability to break when the value of a specific variable changes. These are so-called watchpoints or data breakpoints. JetBrains IDE’s lack this feature unfortunately.
There can also be situations where you would like to temporarily disable breakpoints so you can let your program run normally. In the PyCharm debugger, and in most other debuggers, you can enable/disable individual breakpoints or mute all breakpoints temporarily. In addition, it also possible to remove the breakpoint once hit, so it only breaks once.
Enabling, disabling, or setting new breakpoints can be done during program execution. This is a killer feature and it makes debugging much more enjoyable than with the endless addition and removal of “print” statements.
Once your program hits a breakpoint, you get access to a slew of options to control further execution. You can decide to run the code line-by-line using the step over function. Or you can jump into function calls with step into. There is also the step out option to step to the first line after returning from the current function. These different types of steps let you go up or down levels of your program to reach your desired depth of detail. To understand what imported modules do, it can be useful to step into their functions and follow along.
Some debuggers have the option to do reverse debugging, which essentially gives you the ability to step back as well. Unfortunately, we have yet to see this feature in JetBrains IDEs.
Other available commands are stopping the program completely, restarting the program, continue normally (until the next breakpoint), or run to cursor. The latter is especially useful to let your program reach a point of interest. Besides that, it is also possible to pause a running program manually. But I find this only useful to determine why my program doesn’t seem to progress (in many cases due to an endless loop).
🟥 Stop ‘blogpost’
By now I hope that you have been convinced of all the advantages of a debugger when you deal with code regularly. Their features that let you trace and control program execution will deepen your understanding of any code that crosses your path. Bugs will be easier to solve and head hairs will be spared. In fact, I’d say that once you get used to programming with a debugger, you will never go back.
This is my first Medium post. Let me know your thoughts in the comments! Cheers