How I Debug my Python Projects — Part 1: Raising Exceptions
I’ll start with this quote by Edsger Dijkstra
If debugging is the process of removing software bugs, then programming must be the process of putting them in
Throughout my career, I have worked on many python projects; big, small and medium sized projects. It wasn’t easy at first since I believed that a good programmer should be able to write bug-free code. However, I later understood that the best way to learn is to make mistakes, try, dare and learn from the outcome. I will stop here with this quote by Nicholas Negroponte
Programming allows you to think about thinking, and while debugging you learn learning
Below are the techniques I use in debugging my python projects:
- Raising Exceptions
- Assertions
- Python Logging Module
- Python Debugger Module
- Python Jupyter Notebook
In this article, I will walk you through the first technique — Raising Exceptions
Raising Exceptions
Exceptions are raised when the program encounters an error during its execution. They disrupt the normal flow of the program and usually end it abruptly. Exceptions are raised with the raise statement. In code, a raise statement consists of the following:
- The raise keyword
- A call to the Exception() function
- A string with a helpful error message passed to the Exception() function.
For example, if you enter the following code into your interactive shell
raise Exception(“The error message goes here”)
you will get the following traceback:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Exception: The error message goes here
The traceback includes the error message, the line number of the line that caused the error, and the sequence of the function calls that led to the error. This sequence of calls is called the call stack.
If there is no try
and except
and an optional else
covering the raise statement that raised the exception, the program simply crashes and displays the exceptions error message.
How I Use Exceptions to debug my code
So how do I use exceptions to debug my code? Very good questions. The idea is that I check my code and detect any potential errors that may occur in the future and raise those errors earlier. This way, I can handle the errors before there cause a lot of damage and become difficult to find.
I will explain base on the common exception errors that I encounter while coding.
It is worth noting that, exceptions are mostly considered as user errors. For example, whenever a user inputs an incorrect data or a computation fails because of wrong data, an exception should be generally raised. In most cases, these exceptions are handled and the user is prompted to input the right value. But in other cases, there are not handled and the program is allowed to crash
IndexError
raised when you try to index a list, tuple, or string beyond the permitted boundaries
If in my program, I know I will be dealing with indexing a list, and say the computation that will take place before indexing the list is complex and will take a good amount of time. I won’t want to wait till this computation takes place before receiving the IndexError
exception if it occurs. what I do is I make sure the index I am about to use is in the range. Let’s consider the following code
def compute_get_index(index, arr):
# Do some complex computations on the array
# that may take a lot of time
return arr[index]compute_get_index(8, [3,2,9])
If I run the above function, the indexError
exception will be raised by the program because the index 8
is out of range. The disturbing part of it is that, the complex computation will first execute before the error is raised. So, to detect this bug early, I will check to make sure the index is in range. If not, I raise an exception with a nice message. This way, we get the error and the boring complex computation doesn’t execute
KeyError
raised when you try to access the value of a key that doesn’t exist in a dictionary.
The same principle applies as in IndexError
above. Say you know you will be accessing a dictionary by key, and you are uncertain if the key gotten from either user input or computation is actually in the dictionary. The way I handle this is I first check to make sure it is a key in the dictionary. If it is not, I raise a KeyError
exception early and handle it rather than allowing my code to run just for it to fail with the same error at the end
AttributeError
raised when an invalid attribute reference is made, or when an attribute assignment fails
This is also a very common case. At times, I will call the `append
` method from a None object. Or a `get
` method from a list. This happens because at times what I may be expecting as output from a function or process is not actually it. To debug this, I make sure I check if the object in question has that attribute. If it doesn’t, I raise the AttributeError
exception early, handle it and move on.
From the code above, It shows that I am expecting my object d
to have the attribute append
. For the second case, I am expecting my object d
to be a list
before I carry out my complex computation.
ValueError
Raised when a function receives an argument of the correct type but an inappropriate value
This exception is raised in a lot of mathematical operations. For example, in the math module method math.sqrt()
(square root of a negative number raises a ValueError
exception). If a function works for only a specific range of values, it is reasonable to raise a ValueError
if the argument received doesn’t meet the specific range.
So, if I know my function works only with a range of values, I will make sure to check the value before commencing the long boring complex operation, so that I catch it early, deal with it and move on.
Take for example the function below. I know that my complex operation works only with positive values. So what I do is check to make sure the value that is to be used in the operation is positive. If not, I raise the exception, spare myself from running the complex operation that will obviously fail, fix the issue and move on.
How I build my Custom Exception Classes
We have many other cases which we can handle during debugging like:
- ZeroDivisionError —
raised when you try to divide by zero
- IOError —
raised when an I/O operation fails for an I/O-related reason
- FileExistsError —
raised when trying to create a file or directory which already exists.
- IsADirectoryError —
raised when a file operation is requested on a directory
- NotADirectoryError —
raised when a directory operation is requested on something which is not a directory.
I use all of these to make sure my code works as intended before moving into the more complex part of my code.
In many cases, you may not find a build-in exception that matches the kind of restrain you want to apply in your code. In this case, you can create a custom exception that meets your needs.
Say you want to check and make sure a particular value is greater than 10. Of cause you can use the ValueError
but what if you want to customize the error the more by giving it a specific and unique name like ValueSmallerThanTenError
. This is how I create my custom exception class
- I start with the base class. This is what the Exception doc says
When creating a module that can raise several distinct errors, a common practice is to create a base class for exceptions defined by that module, and subclass that to create specific exception classes for different error conditions
class MyException(Exception):
"""Base class exceptions for this module"""
def __init__(self, msg):
self.msg = msg def __str__(self):
return "{}".format(self.msg)
My base exception has the __init__()
method that takes in the message that is been raised. It also has the __str__()
method because when raising an exception, we are creating an exception instance and printing it at the same time.
- Then, I create my different exception classes that inherit the base class to handle specific errors.
class ValueSmallerThanTenError(MyException):
def __init__(self, msg="ValueSmallerThanTenError occured"):
super().__init__(msg)
Let’s test our custom exception class
Conclusion
These are the ways I use exceptions to debug my code. It may be the wrong way or maybe someone out there has a better way to use Exceptions to debug a python project. Please share your thoughts and idea by commenting.
In my next article, I will show you how I use assertion to debug my code. I hope you enjoyed reading my article and found it helpful.