But what are the exceptions? You can think of them as warning messages letting us know about computation issues and halting the computation. For example, when we're trying to divide by zero, the computer knows it's wrong and raises a corresponding ZeroDivisionError type error, stopping the process. We can raise exceptions from within our own code by using the raise keyword. There are a handful of built-in exception types, such as KeyError, ValueError, and IndexError. The only significant difference between these is the name, which helps us to understand and differentiate between issues. All those exceptions inherit from the base exception type—Exception. Each exception can be raised with supporting text if you so desire. Consider the following example. Here, we declare a function, which checks whether a given instance is of a certain type, using the isinstance function. If it is not, the function will raise a ValueError exception. Predictably, the function won't raise anything when we pass Hello and str as its arguments—indeed, Hello is a string:
def _check_raise(inst, type_, text=None):
text = text if text is not None else f'Wrong value: requires {type} format, got {type(inst)}'
if not isinstance(inst, type_):
raise ValueError(text)
>>> _check_raise('Hello', str)
But when we replace str with int, this throws the error, as intended:
_check_raise('Hello', int)
-------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-50-145b6e9f288c> in <module> ----> 1 _check_raise('Hello', int) <ipython-input-49-b95b042dc54b> in _check_raise(inst, type_, text) 3 4 if not isinstance(inst, type_): ----> 5 raise ValueError(text) ValueError: Wrong value: requires <class 'type'> format, got <class 'str'>
In many cases, it makes sense to create our own exceptions—we'll do that in Chapter 8, Simulation with Classes and Inheritance.
Now that we know about exceptions, let's circle back to the try clause.