# instantiate 2 exceptions
exc1 = Exception()
exc2 = Exception('Hier lief was schief.')
print('Type: ',type(exc1),'Str: ', str(exc1))
print('Type: ', type(exc2),'Str: ', str(exc2))

"""
Explanation: Exceptions
Gernerally speaking, in Python an Exception describes an occured error. Thus, exception handling deals with reacting to errors and deal with them once they occured. The difference between an exception and a malfunction or bug is the expectability of that error. You handle errors that you would also expect with a specific probability. On the other side, a bug is often a logical or syntactical error in your code. Both can not be handled by exception handling. 
A sytactical error will prevent the interpreter from running the code at all, which also includes the error handling. A logical error is not a real error, it's just something that doesn't make any sense. The real source of a logical error is usually sitting in front of the computer. This makes the handling of logical errors the most complicated part in testing and debugging your code. 
Exceptions are instances of the <span style="color:purple">Exception</span> class or an inheriting one. Thus, an Exception can also be isntantiated with a real Exception occuring. All these classes have one thing in common: they accept at least one argument: the error message.
End of explanation
"""


raise exc2

"""
Explanation: There is one more key statement in Python that can only be used for Exceptions: the <span style="color:blue">raise</span> statement. 
This will trigger the Exception. In informatics you will often find the term *'to throw and exception' as well. So, let's raise exc2:
End of explanation
"""


def divide_print(a, b):
    if b == 0:
        print('b must not be zero, idiot!')
    return a / b

"""
Explanation: You could use that to raise your own Exceptions in case the user of your scripts give you wrong input to your functions. Or in case you want to change the sometimes very generic error messages produced by the standard classes.
End of explanation
"""


print(divide_print(5, 0))

"""
Explanation: Print the function
End of explanation
"""


def divide_exc(a, b):
    if b == 0:
        raise Exception('b must not be zero, idiot!')

print(divide_exc(5,0))

"""
Explanation: This time we were able to print a custom error message, but the default Exception is still raised as the program is not interrupted. One option would be to move the return statement into a <span style='color: green'>else</span> clause. The consequence would be a non-explicit function which does return a result only sometime. Additionally, the code gets quite complext for an easy logic like preventing a division by zero. More complex program logics would turn into non-readable code. The other problem of <span style="color:green">None</span>-returning functions os the reusability of the function result, as one would have to check the return values for being <span style="color:green">None</span> everytime.<br>
Therefore it's better to raise your personalized Exception:
End of explanation
"""


import nonexistingpackage

5 / 0

min(5, 4, 'straßenbahn')

open()

foo

# this will only raise an Error on Windows machines
open(5)

fs = open('afile.txt', 'w')
fs.superpower

open('afile.txt', 'k')

"""
Explanation: Now the Exception uses the correct Message. Unfortunately it's just an <span style='color:blue'>Exception</span>, not a <span style='color:blue'>ZeroDivisionError</span> anymore. The most explicit soultion would therefore be to raise an instance of the most suitable child class of Exception using a custom error message. This is easily possible as all of these classes accept an error message as first argument. In case there is no suitable child class, we could easily write our own (which is usually not necessary).
Before doing so, let's raise the different error types:
error types
End of explanation
"""


def divide_exc(a,b):
    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
        raise ValueError('a and b must be numerical. a: {0} b: {1}'.format(type(a), type(b)))
    if b == 0:
        raise ZeroDivisionError('b must not be 0, idiot')
    return a / b

divide_exc(5, 0)

divide_exc(5, None)

"""
Explanation: Inspecting the error types above, either the <span style='color:blue'>ZeroDivisionError or</span> or <span style='color:blue'>ValueError</span> would have been the most appropiate ones.
End of explanation
"""


try:
    divide_exc(5, 0)
except:
    print('Just like something happend.\n')

try:
    divide_exc(5,None)
except ValueError:
    print('This was a ValueError.\n')

try:
    divide_exc(5,0)
except ZeroDivisionError:
    print('definitely a ZeroDivisionError.\n')
    
try:
    divide_exc(5,None)
except ValueError as e:
    print('ALERT! ALERT! ALERT!\nAn critical ValueError occured.\nIt said something like:\n%s.\n-----\n' % str(e))


"""
Explanation: Exception handling
Up to this point we can raise Errors, inherit from existing, write custom error messages or built a new Error class. But how should they be handeled in a project?<br>
It is possible to instruct the python interpreter not to just kill itself on case an error occurs in a specified block of code. Then the interpreter will just interrupt this block and run another block of code for handling the Exception. This handling block will be ignored in case no error occured. It is possible to write a general handling block for all types of errors or to specifiy multiple blocks handling only a specified Error Type. This is especially useful in case only a very specific type of error is expected and all other Exceptions would be unexpected.
End of explanation
"""


def run_divide(a, b):
    try:
        result = 'a / b = {}'.format(divide_exc(a, b))
    except ValueError as e:
        result = 'Error. wrong input.'
    except ZeroDivisionError:
        result = 'a / 1 = {} (b must not be 0, using b=1...)'.format(divide_exc(a, 1))
    
    return result

print('1.: ', run_divide(9, 5))
print('2.: ', run_divide(9, 0))
print('3.: ', run_divide(9, 'five'))

"""
Explanation: Instead of just decorating the error message any handling is possible. One could exit the application, produce a graphical error message, log the exception into a file, continue using default values and so on.<br>
You can even have different <span style='color:green'>except</span> blocks:
End of explanation
"""


def run(a, b):
    try:
        print(divide_exc(a,b))
    except ValueError:
        print('Wrong Input')
    except ZeroDivisionError:
        print('b must not be 0')
    else:
        print('No error occured')
    finally:
        print('---------------\n')
        
run(5,6)
run(3, 'five')
run(4, 0)

"""
Explanation: Any <span style='color:green'>try except</span> construct can also include an <span style='color:green'>else</span> clause and be finished by a <span style='color:green'>finally</span>.
End of explanation
"""


