With a shiny new file open in the text editor and not a line of code written, every new project seems full of possibility and promise. Several thousands of lines of code later, that same project can seem weighed down by bugs that make adding new features a pain, and drain the enthusiasm of programmers. The best software developers know how to find and fix bugs, and they follow software engineering best practices to minimize the occurrence of bugs in the first place.
No programmer will ever write bug free code, but with some practice and determination, it is possible to write clean code, keep bugs in their place and ship reliable software systems.
Your Bug-busting Toolbox
1. Print Statements
The number one tool for debugging code is the tried and true method of inserting print statements. An equivalent alternative, for when print statements become numerous and difficult to manage, is to use a logging system in place of the print statements. Many languages have readily available libraries for this purpose, such as the ‘logging’ library that is built into Python: Logging facility for Python.
Print statements are the fastest, easiest and most direct way for a programmer to inspect the data values and types of variables. Well-placed print statements allow the programmer to track the flow of data through a piece of code and quickly identify the source of the bug.
No matter how many advanced tools come along, the humble print statement should always be the first tool a programmer turns to when trying to debug a piece of code.
Source code debuggers carry the print statement method of debugging to it’s logical conclusion. They allow the programmer to step through code execution line by line and inspect everything from the value of variables to the state of the underlying virtual machine. Most languages have many debuggers available that offer different features, including graphical interfaces, breakpoint settings to halt program execution, and execution of arbitrary code inside of the execution environment.
Employing a debugger can be overkill in many situations, but when it is used properly, a debugger can be a powerful and efficient tool. To understand more about the capabilities of a debugger, check out the Python debugger: pdb.
3. Bug Tracker
Using some sort of bug tracking system is vital for any non-trivial software project. The typical situation that arises when a bug tracker is not used is that programmers need to sort through old emails or chat logs in search of bugs, or even worse, the only documentation of bugs may be in a programmer’s memory. When this happens, some bugs will inevitably go unfixed, and more importantly, it is harder to recognize and address related bugs.
A simple text file can work as an initial bug tracking system for a project. As the code base grows, it won’t take long for the bugs to outgrow a text file. There are many commercial and open-source bug tracking software solutions to choose from. The most important part of choosing which bug tracking software to use is to make sure it is accessible to non-programmers on the project who need to file bugs.
In some languages a linter can perform static analysis on code to recognize problem areas before the code is compiled or run, and in other languages a lint tool is useful for syntax checking and style enforcement. Running a lint program inside of an editor while writing code, or passing code through a linter before compiling or running the code helps programmers find and correct errors before they arise as bugs in the executed software. Using a linter saves significant time tracking down the source of bugs caused by syntactical errors, typos, and incorrect data types.
To get a better idea of what a linter can do, have a look at Pyflakes, a linter for Python: Pyflakes.
5. Version Control
Like using a bug tracking system, using a version control system is a software engineering best practice that any non-trivial sized project cannot afford to ignore. Version control systems like Git, Mercurial and SVN allow different versions of a code base to be separated based on what is being worked on or who is working on the code.
The different versions can then be merged together, so multiple programmers can work on a code base simultaneously, without creating bugs that impair each other’s progress. Version control systems are also crucial because they give programmers the ability to rollback changes to an earlier version of the code, undoing mistakes that would be costly to fix, by simply returning to a state in the code base before the mistakes occurred.
Poorly architected code is a major source of hard-to-fix bugs. When code is easy to understand and can be “executed” mentally or on paper, there is a good chance that programmers can find and fix bugs quickly. The best way to ensure this is to write functions that only do one thing. On the other hand, a piece of code with many responsibilities has many opportunities for errors that are difficult to track down.
Designing software components that handle only one concern is often called code modularization. Modularization helps programmers understand software systems in two ways. First, modularization creates a level of abstraction that makes it possible to think of a module of the system without understanding all of the details, for instance, a programmer building an e-commerce system could think of the credit card processing module and see how it relates to the rest of the code, without having to consider all of the details about credit card processing. Second, the details of a module, like one that handles credit cards, for instance, can be examined and understood without being obfuscated by unrelated code.
7. Automated Tests
Unit tests and other types of automated tests go hand in hand with modularization. An automated test is a piece of code that executes software with specific inputs and checks to see if the program behavior matches what is expected.
Unit tests check the functionality of a single function or class method, while functional tests check a specific program behavior, and integration tests check large parts of a software system or all of the system as a whole. There are many testing frameworks to help make writing tests easier. Many of the popular testing frameworks used today are derived from the JUnit library written by Kent Bent, who was one of the earliest proponents of test-driven development. The Python standard library includes a Python version of JUnit called “PyUnit” or simply “unittest”: Unit testing framework.
8. Teddy Bear Method (Rubber Duck Debugging)
According to programming legends Brain Kernighan and Rob Pike, rubber duck debugging originated in a university computer center where students were required to sit down across from a teddy bear and explain their bugs to it before they could seek help from a live person. This method of debugging is so effective that it spread quickly throughout the entire software engineering world, and like the simple print statement, persists to this day despite the presence of seemingly more sophisticated tools. Nearly anything can be substituted for the teddy bear: rubber ducks are a popular choice, as are patient non-programmers. The important part about this method is to explain the code and the problem out loud in simple and understandable terms.
A similar technique that is also useful is to keep a programming journal where thoughts about the code are recorded before and after implementation.
9. Write Code Comments
Comments should explain the purpose of code on a low-level. As much as possible, the questions of what a line of code does and how it does it should be easily answered by reading the code itself. This is accomplished by writing readable code that is implemented as simply as possible and uses sensible names for functions and variables. The comments around lines of code should fill in the blanks as much as possible, answering questions such as why a particular implementation is used or how a section of code interacts with the rest of the program.
Writing good comments is a solid software engineering practice even in bug-free code, but when bugs arise they can save hours of time spent trying to understand code written days, weeks or months in the past.
10. Write Documentation
While comments describe code at a low-level from a programmer’s point of view, software documentation describes the functionality of a software system, as it is available to users. Depending on the type of software being built, the documentation may describe programming interfaces, graphical interfaces or work flows.
Writing documentation demonstrates an understanding of the software system, and often points out the parts of the system that are not well understood and are a likely source of bugs.
Squashing Bugs Along The Road to Mastery
Computer programming is a craft more than anything, and like other crafts, the path to mastery is paved with diligence and commitment to learning. The job of learning to program is never complete. There are always new things to learn and new ways to improve. Which of these ten debugging tools are you using now? Which of these could you start using today? Which of these tools will require a commitment to set aside time to practice and learn new skills?
Computer programmers enjoy an advantage that few other craftsmen will ever know: all of the best tools and knowledge about programming are readily and freely available for anyone who is interested. You can learn to debug code like a pro, all you have to do is pick up the tools and get to work.