Exception

Exception

Mechanism for interrupting normal program flow and continuing in surrounding context.

  1. event is raising an exception
  2. handling an exception with exception handler
  3. unhandled exceptions cause termination
  4. Exception objects transferred from event to handler

Exception are ubiquitous in Python compare with other programing languages.

exceptional.py with unhandled exception

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
DIGIT_MAP = {
    'zero':  '0',
    'one':   '1',
    'two':   '2',
    'three': '3',
    'four':  '4',
    'five':  '5',
    'six':   '6',
    'seven': '7',
    'eight': '8',
    'nine':  '9',
}

def convert(s):
    number = ''
    for token in s:
        number += DIGIT_MAP[token]
    x = int(number)
    return x

Now let’s make a good call and an exception

1
2
3
4
5
6
7
8
9
>>> from exceptional import convert
>>> convert("one three two".split())
132
>>> convert("seventeen".split())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/vma/decmaxn.github.io/exceptional.py", line 17, in convert
    number += DIGIT_MAP[token]
KeyError: 'seventeen'

Handle KeyError and TypeError with error code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def convert(s):
    try:
        number = ''
        for token in s:
            number += DIGIT_MAP[token] # Exception is raised
        x = int(number) # Skipped when exception is raised
        print(f"Conversion succeeded! x = {x}") # Skipped when exception is raised
    except KeyError: # Program jumped to here when this exception is raised
        print("Conversion failed!")
        x = -1
    except TypeError: # Program jumped to here when this exception is raised
        print("Conversion failed!")
        x = -1
    return x

test

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> from exceptional import convert
>>> convert("one two three".split())
Conversion succeeded! x = 123
123
>>> convert("seventeen".split())
Conversion failed!
-1
>>> convert(123)
Conversion failed!
-1

Programmer Errors

Programmer Errors should not be caught at runtime, etc. IndentationError, SyntaxError and NameError

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def convert(s):
    """Convert a string to an integer."""
    x = -1
    try:
        number = ''
        for token in s:
            number += DIGIT_MAP[token]
        x = int(number)
    except (KeyError, TypeError): # empty block is not permitted and can be solved by adding pass statement as no-op
    return x

Accessing Exception Objects

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import sys

DIGIT_MAP =  . . .

def convert(s):
    try:
        number = ''
        for token in s:
            number += DIGIT_MAP[token]
        return int(number)
    except (KeyError, TypeError) as e: # Use as keyword
        print(f"Conversion error: {e!r}", #Print error message
              file=sys.stderr)
        return -1

Test

1
2
3
4
5
6
7
>>> from exceptional import convert
>>> convert("seventeen".split())
Conversion error: KeyError('seventeen')
-1
>>> convert(123)
Conversion error: TypeError("'int' object is not iterable")
-1

Re-raising Exceptions and clean up action

Much better and altogether more Pythonic is to forget about error return codes completely and go back to raising an exception from convert. Instead of returning an un‑Pythonic error code, we can simply omit our error message and re‑raise the exception object we’re currently handling. This can be done by replacing the return a ‑1 with raise at the end of our exception handling block. Without a parameter, raise simply re‑raises the exception that is being currently handled.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import os
import sys

def make_at(path, dir_name):
    original_path = os.getcwd()
    os.chdir(path)
    try:
        os.mkdir(dir_name)
    except OSError as e:
        print(e, file=sys.stderr)
        raise # Re-raise exception so errors are not passed silently
    finally:  # Clean up action no matter mkdir works or not
        os.chdir(original_path) # try-block terminates