Being able to debug is a core skill for every developer. Do you use a print() statement regularly, or do you prefer to use debugging tools?
Let’s find out how you can improve your debugging experience by understanding the Python stack trace and by using it to fix bugs.
Why Is Debugging Important, and What Does It Cost Organizations?
According to recent Cambridge University research, the global cost of debugging software has risen to $312 billion annually—and debugging takes up half the development time of the average project.
This number is expected to grow as more products and services are digitized. Yet there’s a lack of interest in software debugging—even in the domain of debugging tools and approaches that would make developers’ lives easier.
Even today, many developers rely on the power of the print() command. Are you one of them?
What Is the Impact of Inadequate Debugging?
If you take the easy way out by using a print() statement, then you run the risk of spending 50% of your time debugging code. That makes you partly responsible for higher project costs and delays. However, by regularly adding logging statements to your code, you can make it easier to debug.
Now that computer programs run most electronics, there’s no room for mistakes. Imagine you’re in a car accident. Because of a software bug, your airbag doesn’t go off. In this case, a simple bug can end your life. Also, companies are becoming more aware of the reputation damage that service outages cause—even if those outages are brief.
So, are you debugging the right way? Let’s explore how to use the Python stack trace to your advantage for debugging.
What Is a Python Stack Trace?
Python prints a stack trace when your code throws an exception. A stack trace is often also referred to as a stack traceback, backtrace, or traceback. However, we prefer using the stack trace.
The stack trace might look overwhelming when you see it for the first time. However, the Python stack trace holds a lot of valuable information that helps you identify the source of the problem. Understanding what information a Python stack trace provides is vital to becoming a better Python programmer.
In other words, a stack trace prints all the calls prior to the function that raised an exception. In all cases, the last line of a stack trace prints the most valuable information as here the error gets printed. All information above the thrown error consists of function calls that help you to trace the issue faster.
Example of a Python Stack Trace
A stack trace report contains the function calls made in your code right before the error occurred. When your program raises an exception, it will print the stack trace. Below is an example of a simple Python script that will raise an exception.
# example.py def say(name): print('Hello, ' + nam) say('Michael')
Here, the say() function accepts a parameter name. However, we made a mistake by using the wrong variable inside the print() statement. As you can see, we reference nam instead of name.
When you run the program with python example.py, it should return this stack trace:
As you can see, this stack trace contains a lot of information about what’s gone wrong. First of all, it tells you what type of error has occurred: NameError. This type of exception tells us that we’ve referenced a variable that doesn’t exist. As you read further, it will tell you what variable we tried to reference. Here, nam is not defined. In general, this exception tells you that a variable, function, or class has been referenced incorrectly.
How Can You Read a Python Stack Trace?
Read a stack trace from bottom to top. Why? The last line of a stack trace contains the most recent call. It’s easier to start where things went wrong and work your way up through the code.
Elements of a Python Stack Trace
Let’s learn about the elements that make up a stack trace:
- Blue underline: The last line of a traceback tells you which exception your code has raised.
- Green underline: After the exception, the stack trace prints the error message. The error message contains a more detailed reason your code has thrown this exception. This message helps you analyze the issue and figure out what’s gone wrong.
- Orange boxes: The text in the orange boxes indicates the file, line number, and module name or function specifying where the problematic code is. As you can see, a stack trace may contain many of these that lead you to the actual error.
- Red boxes: The red box shows the actual code that’s been executed. As you might have noticed, a stack trace contains multiple calls that all consist of two lines in the stack trace. This stack trace contains two calls: say() and print(). In this particular stack trace, the error happened inside the print() function.
What Are the Most Common Python Stack Traces?
Knowing how to interpret a Python stack trace is one thing. However, knowing what some of the most common Python stack traces means can speed up your debugging process.
Here are some of the most common Python stack traces you might come across and what they mean.
Your code will throw an AttributeError when you try to access an attribute on an object that doesn’t have this attribute defined. Here’s what the Python Standard Library has to say about this error:
Raised when an attribute reference or assignment fails.
Let’s see how we can reproduce this error:
# example2.py my_num = 1 my_num.param
The following snippet of code will generate this AttributeError.
As you can see, the error message tries to tell us that there’s no attribute “param” on the defined object. The declaration actually contains an integer. It’s a common error, as you’d expect a different type for the object you’re working with.
Your Python script will throw an ImportError when you mess up the import statement. The most common way is by importing a nonexistent module. This is how the Python documentation defines an ImportError exception.
Raised when the import statement has troubles trying to load a module. Also raised when the “from list” in from … import has a name that cannot be found.
Let’s try to reproduce the ImportError:
# example3.py import xyz
The following snippet of code results in the stack trace shown here.
Let’s see how we can get a SyntaxError. According to the official Python documentation, a SyntaxError is raised when:
The parser encounters a syntax error.
A syntax error can be as simple as a missing colon after the function definition:
# example4.py def say(name) print('Hello, ' + name) say('Michael')
The following stack trace printed when you execute the file. As you can see, the ^ (caret) symbol points to where the problem happened. Here, it indicates that some syntax is missing at the end of the function declaration. Unfortunately, it doesn’t tell us exactly what’s wrong with the syntax.
Other common errors include:
- TypeError: “Raised when an operation or function is applied to an object of inappropriate type.”
- ValueError: “Raised when an operation or function receives an argument that has the right type but an inappropriate value, and the situation is not described by a more precise exception such as IndexError.”
- KeyError: “Raised when a mapping (dictionary) key is not found in the set of existing keys.”
- IndexError: “Raised when a sequence subscript is out of range.”
- IOError: “Raised when an I/O operation fails for an I/O-related reason.”
- ZeroDivisionError: This error is very straightforward. The following error is thrown when you try to divide a number by a zero value.
How to Handle Exceptions in Python?
First of all, an exception is an error thrown by your program when it’s executed. Luckily, you can handle those exceptions during the execution of your program so your program doesn’t crash.
When an exception gets thrown, it stops the current code execution and immediately returns the exception. Here, we need to write code to handle those exceptions. Let’s take a look at the AttributeError example.
# example2.py my_num = 1 my_num.param
In order to catch the above exception, we can use the try-except structure which allows you to catch the exception. Let’s rewrite the code to first try the above snippet of code. In case the snippet of code fails, we want to handle the exception.
# example2.py try-except import sys try: my_num = 1 my_num.param except AttributeError: print "Error: Property does not exist" sys.exit()
As you can see, we first try the snippet of code. If it throws an exception, we want to handle the exception. Here, we choose to print a message and then exit the program.
However, you don’t have to exit the program. You can handle the error and call a function again or send an error to the user without exiting the program. Of course, it’s better to not exit the program.
Lastly, we can also use the try-except-finally pattern. The finally clause lets you execute code regardless of the result of the try-except block. Even if an exception is raised, the finally block will execute. For this reason, the finally block is often used for performing a clean up of objects or resources such as a database connection. Here’s an example:
try: myFile = open("file.txt") myFile.write("some text") except IOError: print("Unable to write to file") finally: myFile.close()
If we wouldn’t add the finally block, the final would never be closed in case of an exception as the code execution is stopped immediately. For example, this could cause a memory leak. With the finally block, we can define code that always must be executed regardless of the result of the try-except block.
What Did You Learn About the Python Stack Trace?
The Python stack trace is a valuable piece of information that you can use to debug your code. It contains information about the call stack and points out where things have gone wrong. At the end of a stack trace, you can always find the exact exception type and a detailed message of what’s gone wrong.
If you’re still using print() statements, I hope this article helped you understand why debugging matters and how you can use the Python stack trace to solve bugs.
If you want to learn more about log formatting, check this Scalyr article that talks about some best practices.
Want to decrease your bug resolution time? Check out Scalyr’s log management solution which helps you to quickly identify problems and visualize them. In combination with stack trace data, you should be able to decrease the bug resolution time.
This post was written by Michiel Mulders. Michiel is a passionate blockchain developer who loves writing technical content. Besides that, he loves learning about marketing, UX psychology, and entrepreneurship. When he’s not writing, he’s probably enjoying a Belgian beer!