Exception Handling¶
Introduction¶
Keywords that cover blocks:
try
: try to execute potentially harmful code - might raise exceptionsexcept
: react on those exceptions - catch themfinally
: executed in any case, no matter if raised or notelse
: 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()
raisesFileNotFoundError
when … well … file is not foundtry: 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__()
⟶ printabletry: 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 Nov 19 17:05 /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
andPermissionError
at oncetry: 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 rowtry: 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
andPermissionError
are subclasses ofOSError
... └── OSError ├── FileNotFoundError ├── ... many more ... └── PermissionError
⟶ Catching
OSError
covers bothtry: 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 appearanceTraversal stops at first match
The following is generally unwanted
⟶
OSError
swallows anyopen()
error, preventing specific handling ofFileNotFoundError
andPermissionError
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 exceptionsException
is the root of all non-system-exiting exceptionsUser-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 objectException object is an instance of a class - the exception’s class
No secret here: exceptions are objects like anything else
Can only
raise
subtypes ofBaseException
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 workNot 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 aerrno
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 beforefinally
if any)(Sadly) a syntax error if no
except
clause is presenttry: 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
blockExecuted regardless if an exception was raised or not
Must come last (after any
except
andelse
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