Debugging with Python - Part 1

Debugging with Python Part 1

Errors

Debugging is the process of identifying and fixing bugs (i.e. errors in the code which lead to incorrect software functionality). So, before we discuss “fixing bugs”, let’s explore the types of errors we might make in our programs. We will be looking at the three basic types of errors; namely compilation errors, runtime errors and logical errors.


1. Compilation Errors

Compilation errors occur during the compilation phase and prevent the program from running. These are generally syntax errors (i.e. violations of the syntactic rules of the programming language). However, Python is an interpreted language, and the Python interpreter checks for syntax errors as it reads and parses the script. It won’t run the script and will output an error message indicating the type of compilation error and the line of code so that we can easily rectify our mistake.

Let’s look at two of the most familiar compilation errors we might encounter in our Python code.

Example 1:

print("Hello world")
    print("Hi")

The above code gives the following error message as we have violated the proper indentation rules of Python.
IndentationError: unexpected indent

Example 2:

print(int(3.15)

This code gives the following error message as we have not closed one of the opened parentheses.
SyntaxError: '(' was never closed


2. Runtime Errors

A runtime error is a program error that occurs while the program is running (aka execution time). These are the types of errors that the Python interpreter encounters while trying to execute the code. When a runtime error occurs in the program it will trigger an event named “Exception”. When an exception is raised it will terminate the program unless it is handled by the program. Hence, we could say “unhandled exceptions” lead to runtime errors and handling exceptions during the compilation time is crucial to prevent the program from crashing at the execution time.

There are multiple scenarios which triggers exceptions such as using an undefined variable, performing operations on inappropriate types, using correct types but with inappropriate values, dividing by zero, accessing a non-existing index of a list, and indefinite recursion calls, to name a few. Let’s look at some examples of these scenarios and the types of exceptions they will trigger.

Example 1:

print(x)

This code tries to output x as a variable, but it hasn’t defined the value of x prior, hence it triggers a NameError.
NameError: name 'x' is not defined

Example 2:

print("123" + 4)

This code throws the following error.
TypeError: can only concatenate str (not "int") to str

print(123 + "4")

This code throws the following error.
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Example 3:

num = int("hello")
print(num)

This code is trying to convert a string to an integer. However, the string value is inappropriate hence, it throws a ValueError.
ValueError: invalid literal for int() with base 10: 'hello'

Refer this article to read about type casting.

Example 4:

print(123 / 0)

Trying to divide a number by zero causes an error.
ZeroDivisionError: division by zero

Example 5:

square_numbers = [1, 4, 9, 16]
print(square_numbers[4])

Lists use 0-based indexing, hence the last element of the above list should be square_numbers[3]. Trying to output square_numbers[4] causes an index error.
IndexError: list index out of range

Example 6:

def factorial(num):
    # base case
    # if num == 1:
    #     return 1
    
    # recursive case
    return num * factorial(num - 1)
    
NUM = 3
print(f"Factorial of {NUM} is {factorial(NUM)}")

Let’s use the code example from our “Recursion with Python” article and comment out the base case part. This will trigger the following error.
RecursionError: maximum recursion depth exceeded


3. Logical Errors

Logical errors are more subtle and challenging to identify. They don’t produce syntax errors or cause runtime crashes, but they lead to incorrect program behavior. These errors occur due to mistakes made in the logic or algorithms in the code.

Let’s explore some of the common examples of logical errors in Python.

Example 1:

for i in range(1, 5):
    print(i ** 2)

Assume we write a for loop to calculate the first 5 square numbers. However, our code calculates only the first 4 square numbers. This is because the range function excludes the second argument 5 and only counts from 1 to 4. So, we have to replace the 5 with a 6 to fix this issue.

Example 2:

square_numbers = [1, 4, 9, 16, 25]
print(square_numbers[3])

Assume we have a list of square numbers and we want to extract the third square number, but our code gives us the 4th square number. This is due to Python’s zero-based indexing. Hence, to get the third square number we have to use square_numbers[4]

Example 3:

original = [1, 4, 9]
copy = original	
original.append(16)

print(f"original is {original}, copy is {copy}")

Mutable objects (Ex: list and dictionary) use “assign by reference” principle when assigning an existing list to another variable or “pass by reference” principle when passing a list as an argument in a function invoke (reference is the memory address of the location of the data). Hence, if we want to keep a copy of the list so that we can update the original list, we cannot simply assign the list to a temporary variable as it would apply to both the variables. We have to instead use the copy() function to make a copy of a Python list. So, the line 2 of the above code snippet should be changed as copy = original.copy().

Now that we know commonplace errors we encounter in Python, let’s talk about debugging techniques in the next article.

Comments

Popular posts from this blog

OOP with Python - Part 2

Data Structures - Part 2

OOP with Python - Part 1