5.5. Exceptions

5.5.1. Rationale

  • Used when error occurs

  • You can catch exception and handles erroneous situation

  • Exception example situations:

    • File does not exists

    • No permissions to read file

    • Function argument is invalid type (ie. int('one'))

    • Value is incorrect (ie. negative Kelvin temperature)

    • Network or database connection could not be established

5.5.2. Most Common Exceptions

AttributeError - Attribute reference or assignment fails:

>>> name = 'Jan'
>>> name.append('Twardowski')
Traceback (most recent call last):
AttributeError: 'str' object has no attribute 'append'

ImportError, ModuleNotFoundError - Module could not be located:

>>> import math
>>> import match
Traceback (most recent call last):
ModuleNotFoundError: No module named 'match'

IndexError - Sequence subscript is out of range:

>>> DATA = ['a', 'b', 'c']
>>> DATA[100]
Traceback (most recent call last):
IndexError: list index out of range

KeyError - Dictionary key is not found:

>>> DATA = {'a': 1, 'b': 2}
>>> DATA['x']
Traceback (most recent call last):
KeyError: 'x'

NameError - Local or global name is not found:

>>> print(firstname)
Traceback (most recent call last):
NameError: name 'firstname' is not defined

SyntaxError - Parser encounters a syntax error:

>>> if True
...    print('Yes')
Traceback (most recent call last):
SyntaxError: invalid syntax

IndentationError - Syntax errors related to incorrect indentation:

>>> if True:
...   print('Hello!')
...    print('My name...')
...   print('José Jiménez')
Traceback (most recent call last):
IndentationError: unexpected indent

TypeError - Operation or function is applied to an object of inappropriate type:

>>> 42 + 'a'
Traceback (most recent call last):
TypeError: unsupported operand type(s) for +: 'int' and 'str'
>>> 'a' + 42
Traceback (most recent call last):
TypeError: can only concatenate str (not "int") to str
>>> a = ['a', 'b', 'c']
>>> a[1.5]
Traceback (most recent call last):
TypeError: list indices must be integers or slices, not float
>>> a, b = 1
Traceback (most recent call last):
TypeError: cannot unpack non-iterable int object

ValueError Argument has an invalid value:

>>> a, b, c = 1, 2
Traceback (most recent call last):
ValueError: not enough values to unpack (expected 3, got 2)
>>> a, b = 1, 2, 3
Traceback (most recent call last):
ValueError: too many values to unpack (expected 2)
>>> float('one')
Traceback (most recent call last):
ValueError: could not convert string to float: 'one'
>>> int('one')
Traceback (most recent call last):
ValueError: invalid literal for int() with base 10: 'one'

5.5.3. Exception Hierarchy

BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StopAsyncIteration
      +-- ArithmeticError
      |    +-- FloatingPointError
      |    +-- OverflowError
      |    +-- ZeroDivisionError
      +-- AssertionError
      +-- AttributeError
      +-- BufferError
      +-- EOFError
      +-- ImportError
      +-- 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
      +-- SyntaxError
      |    +-- IndentationError
      |         +-- TabError
      +-- SystemError
      +-- TypeError
      +-- ValueError
      |    +-- UnicodeError
      |         +-- UnicodeDecodeError
      |         +-- UnicodeEncodeError
      |         +-- UnicodeTranslateError
      +-- Warning
           +-- DeprecationWarning
           +-- PendingDeprecationWarning
           +-- RuntimeWarning
           +-- SyntaxWarning
           +-- UserWarning
           +-- FutureWarning
           +-- ImportWarning
           +-- UnicodeWarning
           +-- BytesWarning
           +-- ResourceWarning

5.5.4. Raising Exceptions

Raise Exception without message:

>>> raise RuntimeError
Traceback (most recent call last):
RuntimeError

Exception with additional message:

>>> raise RuntimeError('Some message')
Traceback (most recent call last):
RuntimeError: Some message

5.5.5. Use Case

>>> input = lambda _: -10  # Assume user will input -10
>>>
>>> temperature = input('Type temperature [Kelvin]: ')
>>>
>>> if float(temperature) < 0:
...    raise ValueError('Kelvin temperature cannot be negative')
Traceback (most recent call last):
ValueError: Kelvin temperature cannot be negative
>>> def convert(temperature):
...    if type(temperature) not in {float, int}:
...        raise TypeError('Temperature must be int or float')
...    if temperature < 0:
...        raise ValueError('Kelvin temperature cannot be negative')
...    return temperature
>>> def apollo13():
...    raise RuntimeError('Oxygen tank explosion')
>>>
>>>
>>> apollo13()
Traceback (most recent call last):
RuntimeError: Oxygen tank explosion
>>> def apollo18():
...    raise NotImplementedError('Mission dropped due to budget cuts')
>>>
>>>
>>> apollo18()
Traceback (most recent call last):
NotImplementedError: Mission dropped due to budget cuts

5.5.6. Assertion

  • Raises AssertionError if argument is False

  • Can have optional message

    >>> data = [1, 2, 3]
    >>> assert type(data) is list
    >>> assert all(type(x) is int for x in data)
    
    >>> data = ('a', 'b', 'c')
    >>> assert type(data) is list
    Traceback (most recent call last):
    AssertionError
    >>> assert all(type(x) is int for x in data)
    Traceback (most recent call last):
    AssertionError
    
    >>> import sys
    >>> assert sys.version_info >= (3, 9)
    >>> assert sys.version_info >= (3, 9), 'Python 3.9+ required'
    

5.5.7. Traceback Analysis

  • Stacktrace is 8 levels deep, it's not Java's 200 ;)

    >>> raise RuntimeError
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    RuntimeError
    
    >>> raise RuntimeError('Huston we have a problem')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    RuntimeError: Huston we have a problem
    
    >>> def apollo13():
    ...    raise RuntimeError('Oxygen tank explosion')
    >>>
    >>>
    >>> apollo13()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 2, in apollo13
    RuntimeError: Oxygen tank explosion
    
    >>> def apollo13():
    ...    raise RuntimeError('Oxygen tank explosion')
    >>>
    >>>
    >>> apollo13()
    Traceback (most recent call last):
      File "/home/watney/myscript.py", line 4, in <module>
        apollo13()
      File "/home/watney/myscript.py", line 2, in apollo13
        raise RuntimeError('Oxygen tank explosion')
    RuntimeError: Oxygen tank explosion
    
    >>> def apollo13():
    ...    raise RuntimeError('Oxygen tank explosion')
    >>>
    >>>
    >>> apollo13()
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
      File "/Applications/PyCharm 2019.2 EAP.app/Contents/helpers/pydev/_pydev_bundle/pydev_umd.py", line 197, in runfile
        pydev_imports.execfile(filename, global_vars, local_vars)  # execute the script
      File "/Applications/PyCharm 2019.2 EAP.app/Contents/helpers/pydev/_pydev_imps/_pydev_execfile.py", line 18, in execfile
        exec(compile(contents+"\n", file, 'exec'), glob, loc)
      File "/home/watney/myscript.py", line 4, in <module>
        apollo13()
      File "/home/watney/myscript.py", line 2, in apollo13
        raise RuntimeError('Oxygen tank explosion')
    RuntimeError: Oxygen tank explosion
    

5.5.8. Change Verbosity Level

  • Change level with sys.tracebacklimit

  • From time to time you can have problems somewhere in the middle, but it's rare

  • Last lines are the most important, in most cases error is there

    >>> import sys
    >>> sys.tracebacklimit = 2
    >>>
    >>>
    >>> def apollo13():
    ...    raise RuntimeError('Oxygen tank explosion')
    >>>
    >>> apollo13()
    Traceback (most recent call last):
      File "/home/watney/myscript.py", line 4, in <module>
        apollo13()
      File "/home/watney/myscript.py", line 2, in apollo13
        raise RuntimeError('Oxygen tank explosion')
    RuntimeError: Oxygen tank explosion
    

5.5.9. Catching Exceptions

  • try

  • except

  • else

  • finally

  • try is required and then one of the others blocks

    >>> try:
    ...     # try to execute
    ...     pass
    ... except Exception:
    ...     # what to do if exception occurs
    ...     pass
    ... else:
    ...     # what to do if no exception occurs
    ...     pass
    ... finally:
    ...     # What to do either if exception occurs or not
    ...     pass
    

Catch single exception:

>>> def apollo13():
...    raise RuntimeError('Oxygen tank explosion')
>>>
>>>
>>> try:
...     apollo13()
... except RuntimeError:
...     print('Houston we have a problem')
Houston we have a problem

Catch many exceptions with the same handling:

>>> def apollo13():
...    raise RuntimeError('Oxygen tank explosion')
>>>
>>>
>>> try:
...     apollo13()
... except (RuntimeError, TypeError, NameError):
...     print('Houston we have a problem')
Houston we have a problem

Catch many exceptions with different handling:

>>> try:
...    with open(r'/tmp/my-file.txt') as file:
...         print(file.read())
... except FileNotFoundError:
...     print('File does not exist')
... except PermissionError:
...     print('Permission denied')
File does not exist

Exceptions logging:

>>> import logging
>>>
>>>
>>> def apollo13():
...     raise RuntimeError('Oxygen tank explosion')
>>>
>>> try:
...     apollo13()
... except RuntimeError as err:
...     logging.error(err)
... #stderr: ERROR:root:Oxygen tank explosion

5.5.10. Else and Finally

  • else is executed when no exception occurred

  • finally is executed always (even if there was exception)

  • Used to close file, connection or transaction to database

    >>> try:
    ...     file = open('/tmp/myfile.txt')
    ... except Exception:
    ...     print('Error, file cannot be open')
    ... else:
    ...     file.close()
    Error, file cannot be open
    

else is executed when no exception occurred:

>>> def apollo11():
...     print('Try landing on the Moon')
>>>
>>> try:
...     apollo11()
... except Exception:
...     print('Abort')
... else:
...     print('Landing a man on the Moon')
Try landing on the Moon
Landing a man on the Moon

finally is executed always (even if there was exception):

>>> def apollo11():
...    print('Try landing on the Moon')
>>>
>>> try:
...    apollo11()
... except Exception:
...    print('Abort')
... finally:
...    print('Returning safely to the Earth')
Try landing on the Moon
Returning safely to the Earth
>>> def apollo11():
...    print('Program P63 - Landing Manoeuvre Approach Phase')
...    raise RuntimeError('1201 Alarm')
...    raise RuntimeError('1202 Alarm')
...    print('Contact lights')
...    print('The Eagle has landed!')
...    print("That's one small step for [a] man, one giant leap for mankind.")
>>>
>>> try:
...     apollo11()
... except RuntimeError:
...     print("You're GO for landing")
... except Exception:
...     print('Abort')
... else:
...     print('Landing a man on the Moon')
... finally:
...     print('Returning safely to the Earth')
Program P63 - Landing Manoeuvre Approach Phase
You're GO for landing
Returning safely to the Earth

5.5.11. Pokemon Exception Handling

  • "Gotta catch 'em all"

  • Ctrl-C raises KeyboardInterrupt

User cannot simply kill program with Ctrl-C:

>>> # doctest: +SKIP
... while True:
...    try:
...        number = float(input('Type number: '))
...    except:
...        continue

User can kill program with Ctrl-C:

>>> # doctest: +SKIP
... while True:
...     try:
...         number = float(input('Type number: '))
...     except Exception:
...         continue

5.5.12. Defining Own Exceptions

  • class which inherits from Exception

    >>> class MyError(Exception):
    ...    pass
    >>>
    >>>
    >>> raise MyError
    Traceback (most recent call last):
    MyError
    >>>
    >>> raise MyError('More verbose description')
    Traceback (most recent call last):
    MyError: More verbose description
    

Django Framework Use-case of Custom Exceptions:

>>> # doctest: +SKIP
... from django.contrib.auth.models import User
>>>
>>>
>>> def login(request):
...    username = request.POST.get('username')
...    password = request.POST.get('password')
...
...    try:
...        user = User.objects.get(username, password)
...    except User.DoesNotExist:
...        print('Sorry, no such user in database')

Django Framework Use-case of Custom Exceptions:

>>> class Dragon:
...    def take_damage(self, damage):
...        raise self.IsDead
...
...    class IsDead(Exception):
...        pass
>>>
>>>
>>> wawelski = Dragon()
>>>
>>> try:
...     wawelski.take_damage(10)
... except Dragon.IsDead:
...     print('Dragon is dead')
Dragon is dead

5.5.13. Exit Status Code

  • exit status 0 - no error

  • any other exit status - error

  • This will not work in Jupyter

    >>> try:
    ...     float('hello')
    ... except ValueError:
    ...     print('Cannot type cast to float')
    ...     exit(1)
    Traceback (most recent call last):
    SystemExit: 1
    
$ python3.9 -m doctest myscript.py
$ echo $?
0
$ python3.9 -m doctest myscript.py
**********************************************************************
File "/home/watney/myscript.py", line 41, in myscript
Failed example:
    1 + 2
Expected:
    3
Got:
    4
**********************************************************************
1 items had failures:
   1 of   2 in myscript
***Test Failed*** 1 failures.

$ echo $?
1

5.5.14. Assignments

Code 5.4. Solution
"""
* Assignment: Exception Assert
* Complexity: easy
* Lines of code: 1 lines
* Time: 3 min

English:
    1. Use data from "Given" section (see below)
    2. Use `assert` keyword
    3. Check if current Python version is greater or equal to `required`
    4. If not, raise exception with message 'Python 3.7+ required'

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Użyj słowa kluczowego `assert`
    3. Sprawdź czy obecna wersja Python jest większa lub równa `required`
    4. Jeżeli nie, podnieś wyjątek z komunikatem 'Python 3.7+ required'

Tests:
    >>> type(sys)
    <class 'module'>
    >>> type(current_version)
    <class 'sys.version_info'>
    >>> type(required)
    <class 'tuple'>
    >>> required
    (3, 7)
"""


# Given
import sys
current_version = sys.version_info
required = (3, 7)


Code 5.5. Solution
"""
* Assignment: Exception Raise
* Complexity: easy
* Lines of code: 2 lines
* Time: 3 min

English:
    1. Ask user to input temperature in Kelvins
    2. User will always type proper `int` or `float`
    3. Raise `ValueError` exception if temperature is less than 0

Polish:
    1. Poproś użytkownika o wprowadzenie temperatury w Kelwinach
    2. Użytkownik zawsze poda poprawne `int` lub `float`
    3. Podnieś wyjątek `ValueError` jeżeli temperatura jest poniżej 0

Tests:
    TODO: Doctests
    TODO: Input Stub
    >>> type(temperature)
    <class 'float'>
    >>> import sys
    >>> sys.modules[__name__]
"""


# Given
# temperature = input('Type temperature: ')
temperature = 10
temperature = float(temperature)


Code 5.6. Solution
"""
* Assignment: Exception Except
* Complexity: easy
* Lines of code: 6 lines
* Time: 3 min

English:
    1. Ask user to input temperature in Kelvins
    2. Try converting temperature to `float`
    3. If unsuccessful, then print 'Invalid temperature' and exit with status code 1

Polish:
    1. Poproś użytkownika o wprowadzenie temperatury w Kelwinach
    2. Spróbuj skonwertować temperaturę do `float`
    3. Jeżeli się nie uda to wypisz 'Invalid temperature' i wyjdź z kodem błędu 1

Tests:
    TODO: Doctests
    TODO: Input Stub
"""


# Given
temperature = input('Type temperature: ')


Code 5.7. Solution
"""
* Assignment: Exception Finally
* Complexity: easy
* Lines of code: 2 lines
* Time: 2 min

English:
    1. Ask user to input age
    2. If user has less than 18 years
    3. Raise an exception `PermissionError` with message "Adults only"

Polish:
    1. Poproś użytkownika o wprowadzenie wieku
    2. Jeżeli użytkownik ma mniej niż 18 lat
    3. Wyrzuć wyjątek `PermissionError` z komunikatem "Adults only"

Tests:
    TODO: Doctests
    TODO: Input Stub
"""


# Given
ADULT = 18
age = input('Type age: ')


Code 5.8. Solution
"""
* Assignment: Exception Else
* Complexity: easy
* Lines of code: 2 lines
* Time: 2 min

English:
    1. Ask user to input age
    2. If user has less than 18 years
    3. Raise an exception `PermissionError` with message "Adults only"

Polish:
    1. Poproś użytkownika o wprowadzenie wieku
    2. Jeżeli użytkownik ma mniej niż 18 lat
    3. Wyrzuć wyjątek `PermissionError` z komunikatem "Adults only"

Tests:
    TODO: Doctests
    TODO: Input Stub
"""


# Given
ADULT = 18
age = input('Type age: ')


Code 5.9. Solution
"""
* Assignment: Exception Custom
* Complexity: easy
* Lines of code: 4 lines
* Time: 3 min

English:
    1. Ask user to input angle in degrees
    2. Define custom exception `NegativeKelvin`
    3. If input temperature is lower than 0, raise `NegativeKelvin`

Polish:
    1. Poproś użytkownika o wprowadzenie kąta
    2. Zdefiniuj własny wyjątek `NegativeKelvin`
    3. Jeżeli wprowadzona temperature jest mniejsza niż 0, podnieś `NegativeKelvin`

Tests:
    >>> type(temperature)
    <class 'float'>
    >>> from inspect import isclass
    >>> isclass(NegativeKelvin)
    True
    >>> issubclass(NegativeKelvin, Exception)
    True

TODO: Input Stub
"""


# Given
temperature = input('Type temperature: ')
temperature = float(temperature)


Code 5.10. Solution
"""
* Assignment: Exception Input
* Complexity: easy
* Lines of code: 9 lines
* Time: 5 min

English:
    1. Ask user to input angle in degrees
    2. User can input any value from keyboard (even nonnumeric)
    2. Cotangent for 180 degrees is infinite
    3. Define own exception `CotangentError`
    4. If user typed angle equal to 180, raise your exception

Polish:
    1. Poproś użytkownika o wprowadzenie kąta
    2. Uwaga, użytkownik może podać dowolną wartość z klawiatury (nawet nienumeryczną)
    2. Cotangens dla konta 180 ma nieskończoną wartość
    3. Zdefiniuj własny wyjątek `CotangentError`
    4. Jeżeli użytkownik wprowadził kąt równy 180, podnieś swój wyjątek

Tests:
    TODO: Doctests
    TODO: Input Stub
"""


# Given
degrees = input('Type angle [deg]: ')