Error handling

An exception is an error-caused event that disrupts the normal flow of a program. It can be handled to maintain program stability.

try and except blocks

If we want to prevent errors, we can use the try and except blocks. If inside the try block, an exception is thrown, meaning an error occurs (e.g., the user divides by 0), the rest of the instructions inside this block are skipped, and the except block executes (it will catch this exception).


try:
    x = int(input("Enter a number: "))
except:
    print("Error")
    x = int(input("Try again: "))
                                    

We can add a third block - finally. It executes at the end regardless of what happens in try or except. It is usually used to perform close-up actions, like closing files or connections. It can also exist without the except block (only try and finally).


try:
    x = open("file.txt", "r", encoding = "utf8")
    file.write("text")
except:
    print("Error")
finally:
    x.close()
                                    

The except instruction reacts to every error the same unless we specify what type of error a given instruction may expect (as shown below). This way, we can program different approaches for different errors. We can use many except statements relating to one try block, and if we do, the one without a specified error type (the plain one) has to be the last block.


try:
    x = open("file.txt", "r", encoding = "utf8")
    file.write("text")
    x.close()
except IOError: # input-output error
    print("Could not create the file.")
except:
    print("Other exception.")
                                    

The raise keyword

The raise keyword is used to trigger a specific type of error. It can be used, e.g., inside the try block, to trigger a specific except case, but also outside of try-except to deliberately stop program execution when a critical error occurs. A common use case is validating user input when the provided data does not meet the required format.


x = 0
if x == 0:
    raise ZeroDivisionError("You can't divide by 0.") # it could be also: Exception, SyntaxError, OSError, etc.
                                    

Error examples

  • An IndexError occurs when we try to access an index outside the bounds of, e.g., a list.
  • A KeyError occurs when we try to access a dictionary with a key that does not exist.
  • A TypeError occurs when we perform an invalid operation between incompatible data types.
  • A ValueError occurs when a function receives an argument of the correct type but with an invalid value.
  • A ZeroDivisionError occurs when we attempt to divide a number by zero.
  • A SyntaxError occurs when the Python interpreter encounters code that violates the syntax rules.

The assert keyword

The assert keyword checks whether a given condition is True, and if not, throws an AssertionError with an optional user-written comment. A common use case is debugging complex functions by asserting that input values or intermediate results meet expected conditions before continuing execution.


number = 5
assert number < 0, "The number should be smaller than 0."
                                    

Breakpoints - debugging

If we get an error and we can't fix it, instead of Run, we click Debug. After starting the debugger, we set breakpoints in the "key" moments that we want to analyze. We do that by clicking the margin next to the selected line (a breakpoint is a red dot). The debugger will pause the code on breakpoints so that we can analyze the error (it shows the contents of variables at the moment, etc.) To advance to the next breakpoint, we click F8 on the keyboard or press the "Step Over" button on the screen (in PyCharm). Breakpoints exist in various editors but not in Python IDLE.

The logging module

The logging module allows us to display messages that help us with the debugging process while the program is running. By looking at them, we can figure out what is wrong and fix it. It is different from using print() because after we're done, we can disable these messages (logging.disable()) so we don't have to go through the code and delete them.


import logging
logging.basicConfig(level = logging.DEBUG, format = " %(asctime)s - %(levelname)s - %(message)s")
logging.debug("Start")

def factorial(n):
    logging.debug("Start of factorial(%s%%)" % (n))
    total = 1
    for x in range(n + 1): # for the program to work, this line should look like this: "for x in range(1, n+1):"
        total *= x
        logging.debug("x: " + str(x) + ", the total value: " + str(total))
    logging.debug("End of factorial(%s%%)" % (n))
    return total

print(factorial(5))
logging.debug("End")