Errors and Exceptions#

What you will learn in this lesson:

  • Handling runtime errors with try and except

  • The addition of else and finally clauses

  • Raising exceptions

Introduction#

Errors are mistakes that prevent Python from running your code. These can happen before or during execution.

Examples of errors that happen before execution include syntax errors (SyntaxError) (e.g. forgetting a colon after an if statement, using an invalid variable name…), and an incorrect indentation (IndentationError).

# Here we are missing a colon
if True
    print("This will cause a SyntaxError")
  Cell In[1], line 2
    if True
           ^
SyntaxError: expected ':'
# Here we are missing an indentenation
if True:
print("This will cause a IndentationError")
  Cell In[2], line 3
    print("This will cause a IndentationError")
    ^
IndentationError: expected an indented block after 'if' statement on line 2

There also errors that happen during execution. This are usually called runtime errors.

Exceptions are the way of handling this kind of errors.

Some examples of runtime errors include:

  • NameError happens when one attempts to use a variable that has not been defined

1 + x*3
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[7], line 1
----> 1 1 + x*3

NameError: name 'x' is not defined
  • TypeError happens when operation on an object is not supported.

'2' + 2
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[8], line 1
----> 1 '2' + 2

TypeError: can only concatenate str (not "int") to str
  • ZeroDivisionError happens when you try to divide by zero.

1/0
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[9], line 1
----> 1 1/0

ZeroDivisionError: division by zero

You can find have a complete list of all errors at https://docs.python.org/3/library/exceptions.html

Handling runtime errors with try and except#

We can use try and except to chatch runtime errors and specify how to handle these.

The syntax to use both clauses is the following:

try:
     Some code
except ExceptionType:
     Some other code

Let’s see how it with the following example, which includes a list of numbers where the third one has been included as a string. If you tried to do an arithmetic operation with such an element, it should give you an error (a TypeError specifically) because strings can not perform this kind of operations.

numbers = [1, 2, "3", 4]
for number in numbers:
    try:
        inverse = 1/number
        print(f"1/{number} = {inverse}")
    except TypeError:
        print(f"The number you tried is of type {type(number)}. Maybe it is invalid with the operation you are trying to do?")
1/1 = 1.0
1/2 = 0.5
The number you tried is of type <class 'str'>. Maybe it is invalid with the operation you are trying to do?
1/4 = 0.25

This is how it works:

  • The code within the try clause is executed.

  • If no exception occurs, the block within the except clause is skipped.

  • As soon as an error is encountered within the try block, the execution stops. Now two things may happen:

    • If the error type matches the exception named after the except keyword, that part of the code within the except clause is executed.

    • If the error does not match the exception, it is an unhandled exception and execution stops with an error message.

You can also just print the default message of the error handler as follow:

numbers = [1, 2, "3", 4]
for number in numbers:
    try:
        inverse = 1/number
        print(f"1/{number} = {inverse}")
    except TypeError as e:
        print(f"The number you tried failed because of the following reason:", e)
1/1 = 1.0
1/2 = 0.5
The number you tried failed because of the following reason: unsupported operand type(s) for /: 'int' and 'str'
1/4 = 0.25

Or even do nothing at all, for which you would use the keyword pass. Normally though, this keyword is used as a placeholder for future code.

numbers = [1, 2, "3", 4]
for number in numbers:
    try:
        inverse = 1/number
        print(f"1/{number} = {inverse}")
    except TypeError:
        pass
1/1 = 1.0
1/2 = 0.5
1/4 = 0.25

We can also have multiple except clauses. In this case, we just need to concatenate the exception handlers and the code that we want to execute in that case:

try:
     Some code
except ExceptionType1:
     Some other code
except ExceptionType2:
     More code
except ExceptionType3:
     Even more code

numbers = [0, 1, 2, "3", 4]
for number in numbers:
    try:
        inverse = 1/number
        print(f"1/{number} = {inverse}")
    except TypeError:
        print(f"The number you tried is of type {type(number)}. Maybe it is invalid with the operation you are trying to do?")
    except ZeroDivisionError:
        print("Ups, you are trying to divide by 0..")
Ups, you are trying to divide by 0..
1/1 = 1.0
1/2 = 0.5
The number you tried is of type <class 'str'>. Maybe it is invalid with the operation you are trying to do?
1/4 = 0.25

We can even execute the same block of code for multiple errors. We just need to include multiple exceptions type as tuples after except:

numbers = [0, 1, 2, "3", 4]
for number in numbers:
    try:
        inverse = 1/number
        print(f"1/{number} = {inverse}")
    except (TypeError, ZeroDivisionError):
        print("Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.")
Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.
1/1 = 1.0
1/2 = 0.5
Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.
1/4 = 0.25

Adding finally and else blocks#

  • finally block runs whether or not an exception occurs, and is useful for cleanup actions. It is optional.

numbers = [0, 1, 2, "3", 4]
for number in numbers:
    try:
        inverse = 1/number
        print(f"1/{number} = {inverse}")
    except (TypeError, ZeroDivisionError):
        print("Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.")
    finally:
        print("This always runs")
Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.
This always runs
1/1 = 1.0
This always runs
1/2 = 0.5
This always runs
Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.
This always runs
1/4 = 0.25
This always runs
  • else: The else block executes only when no exception occurs.

numbers = [0, 1, 2, "3", 4]
for number in numbers:
    try:
        inverse = 1/number
        #print(f"1/{number} = {inverse}")
    except (TypeError, ZeroDivisionError):
        print("Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.")
    else:
        print("The division was sucessful!!")
Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.
The division was sucessful!!
The division was sucessful!!
Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.
The division was sucessful!!

But wait… why not including the print in else within the try block?

numbers = [0, 1, 2, "3", 4]
for number in numbers:
    try:
        inverse = 1/number
        print("The division was sucessful!!")
    except (TypeError, ZeroDivisionError):
        print("Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.")
    
Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.
The division was sucessful!!
The division was sucessful!!
Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.
The division was sucessful!!

It works similarly, but if you keep adding code within the try section you may end up having a hard time to identify which piece caused the exception.

So, using else clause keeps your code cleaner and easier to read, since it clearly indicates that its block of code will not run if an exception is raised.

Raising exceptions#

Sometimes you may need to specify certain situations in your code to be exceptional and therefore, needed for your attention. In these cases, you can raise an exception yourself with the keyowrd raise.

age_data = [25, 34, -5, 64, 10]
    
for ii, age in enumerate(age_data):
    try:
        if age < 0:
            raise ValueError("Age cannot be negative")
        print(f"Entry #{ii} has {age} years")
    
    except ValueError as e:
        print(e)
 
Entry #0 has 25 years
Entry #1 has 34 years
Age cannot be negative
Entry #3 has 64 years
Entry #4 has 10 years

Some best practices#

  • Catch specific exceptions, not all exceptions

try:
    result = 10 / 0
except:
    print("Something went wrong!")  # BAD PRACTICE: too vague
Something went wrong!
try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(e) # Specific error
division by zero
  • Provide meaningful messages when raising exceptions.

def meaningful(a, b):
    if b == 0:
        raise ValueError("The denominator cannot be zero.")
    return a / b

try:
    divide(10, 0)
except ValueError as e:
    print(e)  #
The denominator cannot be zero.
  • Always try to handle exceptions appropriately or log them for debugging. It is better to avoid using the pass keyword, unless you are really planning to incorporate future code there

Practice excersises#

Exercise 23

Write a function divide_numbers(a, b) that takes two numbers as input and returns their division result. Catch a ZeroDivisionError if the second number is zero, and a TypeError if either input is not a number. Return appropriate error messages for both cases.

# Your answers here

Exercise 24

Write a function check_positive_number(num) that:

1 - Tries to check if a number is positive.
2 - If the number is negative, raise a ValueError with the message ‘Number must be positive’.
3 - If no exception occurs, print ‘The number is positive’ inside the else block.
4 - Ensure that, regardless of whether an exception occurs, a final message ‘Check complete’ is printed using a finally block.

# Your answers here