21  Exception

Python Exception (realpython)

21.1 Concept

  1. put as little code as possible in the try block. You do this so your except block(s) will not catch or mask errors that they should not.

21.2 Anti-pattern

Don’t ever do this:

try:
    do_something()
except:
    pass

21.3 Raising Exception

x = 10
if x > 5:
    raise Exception('x should not exceed 5. The value of x was: {}'.format(x))
#> Exception: x should not exceed 5. The value of x was: 10

21.4 AssertionError Exception

import sys
assert ('linux' in sys.platform), "This code runs on Linux only."
#> AssertionError: This code runs on Linux only.

21.5 try and except Block

Python executes code following the try statement as a “normal” part of the program. The code that follows the except statement is the program’s response to any exceptions in the preceding try clause.

21.5.1 Ex: Linux function

def linux_interaction():
    assert ('linux' in sys.platform), "Function can only run on Linux systems."
    print('Doing something.')
def mac_interaction():
    assert ('darwin' in sys.platform), "Function can only run on MacOS systems."
    print('Doing something.')
try:
    linux_interaction()
except:
    print('Linux function was not executed')
#> Linux function was not executed
try:
    linux_interaction()
except AssertionError as error:
    print(error)
    print('The linux_interaction() function was not executed')
#> Function can only run on Linux systems.
#> The linux_interaction() function was not executed

21.5.2 Ex: Int & Float

def get_int(x):
    try:
        return int(x)
    except ValueError:
        print(f"'{x}' is not a number")
get_int(5)
#> 5
get_int(-1)
#> -1
get_int(5.6)
#> 5
get_int("cat")
#> 'cat' is not a number
def attempt_float(x):
    try:
        return float(x)
    except ValueError:
        return x
attempt_float("2")
#> 2.0
attempt_float("X")
#> 'X'

using pass to return NoneType and not print anything

def get_int2(x):
    try:
        return int(x)
    except ValueError:
        pass
get_int2(2)
#> 2
get_int2("Cat") # Nothing

type(get_int2("Cat"))
#> <class 'NoneType'>

isnumeric() to check whether string is all numeric. But, it’s not very effective:

"123".isnumeric() # Only case that works
#> True
"12.3".isnumeric()
#> False
"-1".isnumeric()
#> False
"cat".isnumeric()
#> False

21.5.3 Ex: Try open file

try:
    with open('file.log') as file:
        read_data = file.read()
except:
    print('Could not open file.log')
#> Could not open file.log
try:
    with open('file.log') as file:
        read_data = file.read()
except FileNotFoundError as fnf_error:
    print(fnf_error)
#> [Errno 2] No such file or directory: 'file.log'
try:
    linux_interaction() # <-- AssertionError trigger in this line
    with open('file.log') as file:
        read_data = file.read()
except FileNotFoundError as fnf_error:
    print(fnf_error)
except AssertionError as error: 
    print(error) 
    print('Linux linux_interaction() function was not executed')
#> Function can only run on Linux systems.
#> Linux linux_interaction() function was not executed
try:
    mac_interaction()
    with open('file.log') as file: # <-- FileNotFoundError trigger in this line
        read_data = file.read()
except FileNotFoundError as fnf_error:
    print(fnf_error)
except AssertionError as error:
    print(error)
    print('Linux mac_interaction() function was not executed')
#> Doing something.
#> [Errno 2] No such file or directory: 'file.log'

21.6 try and except catch error

21.6.1 Ex: Atomic numbers of noble gases.

nobles = {'He': 2, 'Ne': 10, 'Ar': 18, 'Kr': 36, 'Xe': 54}

def show_element_info(elements):
   for element in elements:
       print('Atomic number of {} is {}'.format(
             element, nobles[element]))
try:
    show_element_info(['Ne', 'Ar', 'Br'])
except KeyError as err:
    missing_element = err.args[0]
    print(f"Error args: {err.args}")
    print(f"Missing data for element: {missing_element}")
#> Atomic number of Ne is 10
#> Atomic number of Ar is 18
#> Error args: ('Br',)
#> Missing data for element: Br

21.6.2 Simple Logging

import logging
UPLOAD_ROOT = "fold/testdir"

def create_upload_dir(username):
    userdir = os.path.join(UPLOAD_ROOT, username)
    try:
        os.makedirs(userdir)
    except FileExistsError as err:
        logging.error("Upload dir already exists: %s",
            err.filename)
create_upload_dir("testuser")
#> NameError: name 'os' is not defined. Did you forget to import 'os'

21.7 else clause

try:
    mac_interaction() # Pass
except AssertionError as error:
    print(error)
else:
    print('Executing the else clause.')
#> Doing something.
#> Executing the else clause.

21.7.1 try-except-else-finally

try:
    mac_interaction()
except AssertionError as error:
    print(error)
else:
    try:
        with open('file.log') as file:
            read_data = file.read()
    except FileNotFoundError as fnf_error:
        print(fnf_error)
finally:
    print('Cleaning up, irrespective of any exceptions.')
#> Doing something.
#> [Errno 2] No such file or directory: 'file.log'
#> Cleaning up, irrespective of any exceptions.

21.7.2 Always close File

f = open(path, mode="w")

try:
    write_to_file(f)
except:
    print("Failed")
else:
    print("Succeeded")
finally:
    f.close()

21.8 Custom Exception

Custom exceptions initialize by creating a class that inherits from the base Exception class of Python

class MyCustomException(Exception):
    pass
raise MyCustomException('A custom message for my custom exception')
#> MyCustomException: A custom message for my custom exception
try:
    raise MyCustomException('A custom message for my custom exception')
except MyCustomException:
    print('My custom exception was raised')
#> My custom exception was raised

21.9 Full stack trace by logging

Python provides an easy way to capture that error event, and all the information you need to fix it. The logging module has a function called exception(), which will log your message along with the full stack trace of the current exception. So you can write code like this:

import logging

def get_number():
    return int('foo')

try:
    x = get_number()
except:
    logging.exception('Caught an error')
#> ERROR:root:Caught an error
#> Traceback (most recent call last):
#>   File "<string>", line 3, in <module>
#>   File "<string>", line 3, in get_number
#> ValueError: invalid literal for int() with base 10: 'foo'