Exception Handling

Introduction

Keywords that cover blocks:

  • try: try to execute potentially harmful code - might raise exceptions

  • except: react on those exceptions - catch them

  • finally: executed in any case, no matter if raised or not

  • else: executed if no exceptions was raised

A few examples are in order …

Basic Exception Handling: try, except

  • Catching an exception no matter what

    try:
        f = open('file-that-does-not-exist.txt')
    except:                     # <--- unconditionally catching *any* error
        print('bad luck')
    
    bad luck
    
  • Usually not a good idea: covers other more severe errors

    try:
        print(a_variable)       # <--- raises NameError!
        f = open('file-that-does-not-exist.txt')
    except:
        print('bad luck')
    
    bad luck
    
  • ⟶ catch exception by their type

Catching Exceptions By Type

  • More specific reaction on errors: by type

  • E.g.: open() raises FileNotFoundError when … well … file is not found

    try:
        f = open('file-that-does-not-exist.txt')
    except FileNotFoundError:
        print('file not there')
    
    file not there
    

Exception Objects

  • Exceptions are objects

  • Can carry anything that’s relevant to the error

  • Usually implement __str__() ⟶ printable

    try:
        f = open('file-that-does-not-exist.txt')
    except FileNotFoundError as e:
        print('file not there:', e)
    
    file not there: [Errno 2] No such file or directory: 'file-that-does-not-exist.txt'
    

Catching Multiple Exception Types: Exception List

  • Different error at the same level: PermissionError

  • E.g. when a file has no read permissions

    ----------. 1 jfasch jfasch 0 Apr  4 16:02 /tmp/some-file.txt
    
    try:
        open('/tmp/some-file.txt')
    except PermissionError as e:
        print('bad luck on permissions:', e)
    
    bad luck on permissions: [Errno 13] Permission denied: '/tmp/some-file.txt'
    
  • Catching both FileNotFoundError and PermissionError at once

    try:
        open('/tmp/some-file.txt')
    except (FileNotFoundError, PermissionError) as e:
        print('either file not there, or bad luck on permissions:', e)
    
    either file not there, or bad luck on permissions: [Errno 13] Permission denied: '/tmp/some-file.txt'
    

Catching Multiple Exception Types: Multiple except Clauses

  • Specific handling of a number of different exceptions

  • ⟶ multiple except clauses in a row

    try:
        open('/tmp/some-file.txt')
    except FileNotFoundError as e:
        print('file not there:', e)
    except PermissionError as e:
        print('bad luck on permissions:', e)
    
    bad luck on permissions: [Errno 13] Permission denied: '/tmp/some-file.txt'
    

Catching Multiple Exception Types: By Base Type

  • Both FileNotFoundError and PermissionError are subclasses of OSError

    ...
     └── OSError
          ├── FileNotFoundError
          ├── ... many more ...
          └── PermissionError
    
  • ⟶ Catching OSError covers both

    try:
        open('/tmp/some-file.txt')
    except OSError as e:            # <--- FileNotFoundError and PermissionError (and ...)
        print('bad luck, OS-wise:', e)
    
    bad luck, OS-wise: [Errno 13] Permission denied: '/tmp/some-file.txt'
    

Important: Order Of except Clauses

  • except clauses are evaluated in order of appearance

  • Traversal stops at first match

  • The following is generally unwanted

  • OSError swallows any open() error, preventing specific handling of FileNotFoundError and PermissionError

    try:
        open('/tmp/some-file.txt')       # <--- raises PermissionError
    except OSError as e:                 # <--- matches PermissionError (which is-a OSError)
        print('bad luck, OS-wise:', e)
    except FileNotFoundError as e:       # <--- skipped
        print('file not there:', e)
    except PermissionError as e:         # <--- skipped
        print('bad luck on permissions:', e)
    
    bad luck, OS-wise: [Errno 13] Permission denied: '/tmp/some-file.txt'
    
  • Put more specific errors at the top

  • Base classes at the bottom

  • sort by specificity

  • ⟶ fallback error handling

    try:
        open('/tmp/some-file.txt')
    except FileNotFoundError as e:
        print('file not there:', e)
    except PermissionError as e:
        print('bad luck on permissions:', e)
    except OSError as e:               # <--- fallback for other types of OSError
        print('bad luck, OS-wise:', e)
    
    bad luck on permissions: [Errno 13] Permission denied: '/tmp/some-file.txt'
    

Built-In Exception Hierarchy

  • BaseException is the root of all exceptions

  • Exception is the root of all non-system-exiting exceptions

  • User-defined exceptions should derive from Exception

  • (Not a hard rule though)

BaseException
 ├── BaseExceptionGroup
 ├── GeneratorExit
 ├── KeyboardInterrupt
 ├── SystemExit
 └── Exception
      ├── ArithmeticError
      │    ├── FloatingPointError
      │    ├── OverflowError
      │    └── ZeroDivisionError
      ├── AssertionError
      ├── AttributeError
      ├── BufferError
      ├── EOFError
      ├── ExceptionGroup [BaseExceptionGroup]
      ├── ImportError
      │    └── ModuleNotFoundError
      ├── LookupError
      │    ├── IndexError
      │    └── KeyError
      ├── MemoryError
      ├── NameError
      │    └── UnboundLocalError
      ├── OSError
      │    ├── BlockingIOError
      │    ├── ChildProcessError
      │    ├── ConnectionError
      │    │    ├── BrokenPipeError
      │    │    ├── ConnectionAbortedError
      │    │    ├── ConnectionRefusedError
      │    │    └── ConnectionResetError
      │    ├── FileExistsError
      │    ├── FileNotFoundError
      │    ├── InterruptedError
      │    ├── IsADirectoryError
      │    ├── NotADirectoryError
      │    ├── PermissionError
      │    ├── ProcessLookupError
      │    └── TimeoutError
      ├── ReferenceError
      ├── RuntimeError
      │    ├── NotImplementedError
      │    └── RecursionError
      ├── StopAsyncIteration
      ├── StopIteration
      ├── SyntaxError
      │    └── IndentationError
      │         └── TabError
      ├── SystemError
      ├── TypeError
      ├── ValueError
      │    └── UnicodeError
      │         ├── UnicodeDecodeError
      │         ├── UnicodeEncodeError
      │         └── UnicodeTranslateError
      └── Warning
           ├── BytesWarning
           ├── DeprecationWarning
           ├── EncodingWarning
           ├── FutureWarning
           ├── ImportWarning
           ├── PendingDeprecationWarning
           ├── ResourceWarning
           ├── RuntimeWarning
           ├── SyntaxWarning
           ├── UnicodeWarning
           └── UserWarning

Raising Exceptions

  • raise an exception object

  • Exception object is an instance of a class - the exception’s class

  • No secret here: exceptions are objects like anything else

  • Can only raise subtypes of BaseException though

def maybe_fail(answer):
    if answer != 42:
        raise RuntimeError('wrong answer')

maybe_fail(666)
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[12], line 5
      2     if answer != 42:
      3         raise RuntimeError('wrong answer')
----> 5 maybe_fail(666)

Cell In[12], line 3, in maybe_fail(answer)
      1 def maybe_fail(answer):
      2     if answer != 42:
----> 3         raise RuntimeError('wrong answer')

RuntimeError: wrong answer

Re-Raising Exceptions

  • Want to only shortly intercept an exception on its way through

  • Otherwise pass it on unmodified

  • ⟶ a lone raise statement

def maybe_fail(answer):
    if answer != 42:
        raise RuntimeError('wrong answer')

try:
   maybe_fail(666)
except RuntimeError as e:
   print('argh!')
   raise             # <--- re-raise same exception
argh!
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[13], line 6
      3         raise RuntimeError('wrong answer')
      5 try:
----> 6    maybe_fail(666)
      7 except RuntimeError as e:
      8    print('argh!')

Cell In[13], line 3, in maybe_fail(answer)
      1 def maybe_fail(answer):
      2     if answer != 42:
----> 3         raise RuntimeError('wrong answer')

RuntimeError: wrong answer

User-Defined Exceptions

  • Built-in exceptions can be raised in user code

  • RuntimeError is a good candidate when defining one’s own exception hierarchy is too much work

  • Not always enough though

  • Convention, not law: derive user-defined exceptions from Exception

Minimal hierarchy - just the types are of interest …

class MySubsystemError(Exception):
    pass

class ReallyBadError(MySubsystemError):
    pass

class SomeOtherError(MySubsystemError):
    pass

User-Defined Exceptions: More

  • Why not store common data (e.g. OSError has a errno attribute) …

  • A possible error scheme would be as follows

    (DefinitelyBad, EvenWorse, CollapsingTheWorld) = range(1, 4)
    
    class MySubsystemError(Exception):           # <--- common base class for all subsystem errors
        def __init__(self, msg, errorcode):
            super().__init__(msg)
            self.errorcode = errorcode
        def __str__(self):
            return super().__str__() + f' ({self.errorcode})'
    
    class ReallyBadError(MySubsystemError):      # <--- one error
        pass
    
    class SomeOtherError(MySubsystemError):      # <--- another error
        pass
    
  • The “subsystem” implementation

    def foo(answer):
        if answer != 42:
            raise ReallyBadError(f'Bad answer: {answer}', DefinitelyBad)
    
  • “subsystem” usage

    try:
        foo(666)
    except MySubsystemError as e:                # <--- only interested in base type
        print(e)
    
    Bad answer: 666 (1)
    

else: Executed If No Exception

  • Separation of concerns

  • Executed if no exception was raised

  • Must come directly after except clauses (and before finally if any)

  • (Sadly) a syntax error if no except clause is present

    try:
        open('/etc/passwd')        # <--- succeeds
    except OSError as e:           # <--- must be there (syntax error otherwise)
        print('bad luck, OS-wise:', e)
    else:
        print('all well')
    
    all well
    

finally: Executed Regardless Of Exception

  • Separation of concerns

  • Error-unrelated things done in finally block

  • Executed regardless if an exception was raised or not

  • Must come last (after any except and else clauses)

  • Here the error case:

    try:
        open('/tmp/some-file.txt')           # <--- fails
    except OSError as e:
        print('bad luck, OS-wise:', e)
    finally:
        print('doing error-unrelated stuff')
    
    bad luck, OS-wise: [Errno 13] Permission denied: '/tmp/some-file.txt'
    doing error-unrelated stuff
    
  • And the sunny case

    try:
        open('/etc/passwd')                  # <--- succeeds
    except OSError as e:
        print('bad luck, OS-wise:', e)
    finally:
        print('doing error-unrelated stuff')
    
    doing error-unrelated stuff