Exception handling

This chapter will discuss different types of errors and how to handle some of the them within the program gracefully. You'll also see how to raise exceptions programmatically.

Syntax errors

Quoting from docs.python: Errors and Exceptions:

There are (at least) two distinguishable kinds of errors: syntax errors and exceptions

Here's an example program with syntax errors:

# syntax_error.py
print('hello')

def main():
    num = 5
    total = num + 09 
    print(total)

main)

The above code is using an unsupported syntax for a numerical value. Note that the syntax check happens before any code is executed, which is why you don't see the output for the print('hello') statement. Can you spot the rest of the syntax issues in the above program?

$ python3.9 syntax_error.py
  File "/home/learnbyexample/Python/programs/syntax_error.py", line 5
    total = num + 09 
                   ^
SyntaxError: leading zeros in decimal integer literals are not permitted;
             use an 0o prefix for octal integers

try-except

Exceptions happen when something goes wrong during the code execution. For example, passing a wrong data type to a function, dividing a number by 0 and so on. Such errors are typically difficult or impossible to determine just by looking at the code.

>>> int('42')
42
>>> int('42x')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: '42x'

>>> 3.14 / 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: float division by zero

When an exception occurs, the program stops executing and displays the line that caused the error. You also get an error type, such as ValueError and ZeroDivisionError seen in the above example, followed by a message. This may differ for user defined error types.

You could implement alternatives to be followed for certain types of errors instead of premature end to the program execution. For example, you could allow the user to correct their input data. In some cases, you want the program to end, but display a user friendly message instead of developer friendly traceback.

Put the code likely to generate an exception inside try block and provide alternate path(s) inside one or more except blocks. Here's an example to get a positive integer number from the user, and continue doing so if the input was invalid.

# try_except.py
from math import factorial

while True:
    try:
        num = int(input('Enter a positive integer: '))
        print(f'{num}! = {factorial(num)}')
        break
    except ValueError:
        print('Not a positive integer, try again')

It so happens that both int() and factorial() generate ValueError in the above example. If you wish to take the same alternate path for multiple errors, you can pass a tuple to except instead of a single error type. Here's a sample run:

$ python3.9 try_except.py
Enter a positive integer: 3.14
Not a positive integer, try again
Enter a positive integer: hi
Not a positive integer, try again
Enter a positive integer: -2
Not a positive integer, try again
Enter a positive integer: 5
5! = 120

You can also capture the error message using the as keyword (which you have seen previously with import statement, and will come up again in later chapters). Here's an example:

>>> try:
...     num = 5 / 0
... except ZeroDivisionError as e:
...     print(f'oops something went wrong! the error msg is:\n"{e}"')
... 
oops something went wrong! the error msg is:
"division by zero"

info See docs.python: built-in exceptions for documentation on built-in exceptions.

warning It is not recommended to use except without passing an error type. See stackoverflow: avoid bare exceptions for details.

info There are static code analysis tools like pylint, "which looks for programming errors, helps enforcing a coding standard, sniffs for code smells and offers simple refactoring suggestions". See awesome-python: code-analysis for more such tools.

else

The else clause behaves similarly to the else clause seen with loops. If there's no exception raised in the try block, then the code in the else block will be executed. This block should be defined after the except block(s). As per the documentation:

The use of the else clause is better than adding additional code to the try clause because it avoids accidentally catching an exception that wasn’t raised by the code being protected by the try ... except statement.

# try_except_else.py
while True:
    try:
        num = int(input('Enter an integer number: '))
    except ValueError:
        print('Not an integer, try again')
    else:
        print(f'Square of {num} is {num ** 2}')
        break

Here's a sample run:

$ python3.9 try_except_else.py
Enter an integer number: hi
Not an integer, try again
Enter an integer number: 3.14
Not an integer, try again
Enter an integer number: 42x
Not an integer, try again
Enter an integer number: -2
Square of -2 is 4

raise

You can also manually raise exceptions if needed. It accepts an optional error type, which can be either a built-in or a user defined one (see docs.python: User-defined Exceptions). And you can optionally specify an error message. raise by itself re-raises the currently active exception, if any (RuntimeError otherwise).

>>> def sum2nums(n1, n2):
...     types_allowed = (int, float)
...     if type(n1) not in types_allowed or type(n2) not in types_allowed:
...         raise TypeError('Argument should be an integer or a float value')
...     return n1 + n2
... 
>>> sum2nums(3.14, -2)
1.1400000000000001
>>> sum2nums(3.14, 'a')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in sum2nums
TypeError: Argument should be an integer or a float value

finally

You can add code in finally block that should always be the last thing done by the try statement, irrespective of whether an exception has occurred. This should be declared after except and the optional else blocks.

# try_except_finally.py
try:
    num = int(input('Enter a positive integer: '))
    if num < 0:
        raise ValueError
except ValueError:
    print('Not a positive integer, run the program again')
else:
    print(f'Square root of {num} is {num ** 0.5:.3f}')
finally:
    print('\nThanks for using the program, have a nice day')

Here's some sample runs when the user enters some value:

$ python3.9 try_except_finally.py
Enter a positive integer: -2
Not a positive integer, run the program again

Thanks for using the program, have a nice day
$ python3.9 try_except_finally.py
Enter a positive integer: 2
Square root of 2 is 1.414

Thanks for using the program, have a nice day

Here's an example where something goes wrong, but not handled by the try statement. Note that finally block is still executed.

# here, user presses Ctrl+D instead of entering a value
# you'll get KeyboardInterrupt if the user presses Ctrl+C
$ python3.9 try_except_finally.py
Enter a positive integer: 
Thanks for using the program, have a nice day
Traceback (most recent call last):
  File "/home/learnbyexample/Python/programs/try_except_finally.py",
       line 2, in <module>
    num = int(input('Enter a positive integer: '))
EOFError

See docs.python: Defining Clean-up Actions for details like what happens if an exception occurs within an else clause, presence of break/continue/return etc. The documentation also gives examples of where finally is typically used.

Exercises

  • Identify the syntax errors in the following code snippets. Try to spot them manually.

    # snippet 1:
    def greeting()
        print('hello')
    
    # snippet 2:
    num = 5
    if num = 4:
        print('what is going on?!')
    
    # snippet 3:
    greeting = “hi”
    
  • In case you didn't complete the exercises from Importing your own module section, you should be able to do it now.

  • Write a function num(ip) that accepts a single argument and returns the corresponding integer or floating-point number contained in the argument. Only int, float and str should be accepted as valid input data type. Provide custom error message if the input cannot be converted to a valid number. Examples are shown below:

    >>> num(0x1f)
    31
    >>> num(3.32)
    3.32
    >>> num(' \t 52 \t')
    52
    >>> num('3.982e5')
    398200.0
    
    # wrong data type
    >>> num(['1', '2.3'])
    TypeError: not a valid input
    # string input that cannot be converted to a valid int/float number
    >>> num('foo')
    ValueError: could not convert string to int or float