## Input errors

One of the most tedious jobs in writing code designed to be used by others is checking a user’s input to make sure it’s valid. As an example, we return to our program that calculates the area of a polygon. We have a Polygon class in Polygon.py:

```from math import *

class Polygon:
def __init__(self, numSides, sideLength) :
self.__numSides = numSides
self.__sideLength = sideLength
if numSides == 3:
self.__area = sqrt(3) * sideLength * sideLength / 4
elif numSides == 4:
self.__area = sideLength * sideLength
elif numSides == 5:
self.__area = sqrt(5 * (5 + 2*sqrt(5))) * sideLength * sideLength / 4

def getArea(self):
return self.__area
```

This class expects two inputs to the constructor: `numSides`, which should be an int with the value 3, 4 or 5, and `sideLength`, which should be a float. If these two conditions are satisfied, the constructor calculates the area of the polygon.

Now suppose we write a program (in main.py) that asks the user to input these two values and then creates a Polygon object to calculate the area. We might try this:

```from Polygon import *

while True:
polyData = input('Number of sides & side length: ')
if polyData == 'quit': break

polyData = polyData.split()
numSides = int(polyData)
sideLength = float(polyData)
poly = Polygon(numSides, sideLength)
print(f'Area = {poly.getArea()}')
```

This will work fine provided the user enters the two bits of data in the required format. However, there are several errors the user might make when entering the data.

1. The first entry is not a valid int.
2. The first entry is a valid int, but is not 3, 4 or 5.
3. The second entry is not a valid float.
4. Only one value is entered.

We could write code using various type-checking functions and if statements to check all these things, but a cleaner way of handling input errors is by using exceptions.

## Exceptions: the try statement

To begin, it’s a good idea to see what happens when each of the above errors is made by a user. We find:

1. Suppose we enter `d 4.5`, so that the first entry is not a valid int. The program gives the error: “ValueError: invalid literal for int() with base 10: ‘d'”.
2. Next, we try entering `1 4.5`, so that the first entry is a valid int, but is not 3, 4 or 5. This time, we get the error: “AttributeError: ‘Polygon’ object has no attribute ‘_Polygon__area'”. This might be a bit mystifying, but if we refer back to the Polygon class definition above, we see that if `numSides` isn’t 3, 4 or 5, the `__area` attribute is never calculated, so it doesn’t exist.
3. Next, we try `3 d`, so that the first entry is a valid int in the correct range, but the second entry is not a valid float. Now we get the error: “ValueError: could not convert string to float: ‘d'”.
4. Finally, we try entering only a single value, such as `3`. This time we get the error: “IndexError: list index out of range”. This is because we expect `polyData` to have 2 elements with indexes  and , and we have only the  element.

Note that in all cases, the first thing Python prints out is the name of an error, such as `ValueError`, `AttributeError` and so on. These are all names of built-in classes that are examples of exceptions. An exception is generated (the technical term is raise; users of other languages might be used to the keyword throw, which is the same thing) whenever something goes wrong during the execution of a program. If the coder doesn’t take any action when an exception is raised, the default behaviour is to print an error message and stop the program.

## Handling built-in exceptions

Python provides a large number of built-in exceptions. A list of these is provided here. Keep in mind that this list may change as newer versions of Python are released. The built-in exception classes all ultimately inherit the `BaseException` class.

If you want to catch an exception in your code and provide some user-friendly feedback (and also prevent the program from crashing) you can use the `try` statement. The idea is that you identify a block of code which could potentially raise an exception and enclose this block inside a `try` statement. Following the `try`, you write one or more `except` blocks, each of which can deal with one (or more) class of exception.

As a first attempt, we deal with a `ValueError` and an `IndexError`. Here’s a rewrite of the above main.py program:

```from Polygon import *

while True:
polyData = input('Number of sides & side length: ')
if polyData == 'quit': break

polyData = polyData.split()
try:
numSides = int(polyData)
except ValueError as err:
print('Invalid quantity for number of sides:', err)
else:
try:
sideLength = float(polyData)
poly = Polygon(numSides, sideLength)
print(f'Area = {poly.getArea()}')
except ValueError as err:
print('Invalid quantity for side length:', err)
except IndexError:
print('Enter number of sides and side length (2 items)')```

Several features of a `try` statement are illustrated here. On line 8, we start a `try` block. We see from above that there are two cases that can generate a `ValueError`: either `numSides` is not a valid int, or `sideLength` is not a valid float. If we just enclosed both lines 9 and 14 inside the same `try` block, we would have no way of telling which error raised the `ValueError` exception (although the error message does tell us something, it’s not in a very user-friendly form). So we isolate line 9 in a `try` block on its own, and if the user enters `numSides` in an incorrect format, a `ValueError` will be raised at this point.

If a `ValueError` occurs, control skips from line 9 to the first `except` statement following the `try`. The `except` statement specifies the name of the exception that it is looking for, and if the exception just raised matches this type, the code inside the `except` block is run. On line 10, we are looking for a `ValueError`, and if such an exception has occurred, we name the instance of `ValueError` as `err`. Remember that `ValueError` is the class of exception, and `err` is the particular instance (object) of this class that has been raised. On line 11, we print out a message saying that the number of sides is incorrect, followed by `err`. Printing an exception object prints out the default error message that the exception generated. Thus if we entered `d 4.5` into this program, we would get the message “Invalid quantity for number of sides: invalid literal for int() with base 10: ‘d'” printed out.

Crucially, the program does not crash after this error message. Control passes to the first statement following the try, which in this case reverts back to the while statement on line 3. To see why this is the next valid statement to be executed, we need to look at the rest of the program.

Line 12 illustrates the `else` block of a `try` statement. The `else` block is run only if no exceptions were raised by the parent `try` block. In our example, the `try` block contains only one statement (line 9), so if this doesn’t raise any exceptions (that is, if the value for `numSides` is valid) then the code in the `else` block is run, starting on line 13.

If we reach line 13, then `numSides` is valid, so we can continue by checking that `sideLength` is valid. As we mentioned above, two things might go wrong at this stage: the user might have entered an invalid quantity for `sideLength`, or they might have entered only one item, so that the `polyData` list contains only one item. The first of these would raise a `ValueError`, while the second would raise an `IndexError`.

We provide a separate `except` block for each of these two cases. As the `IndexError` will occur only if the user has entered too few data items, we don’t need to refer to its default message, so we don’t bother giving the `IndexError` object a name. We just print our own error message.

If the user has entered a correct set of two data values, then no exceptions are raised, and only lines 9 and 14 through 16 are executed.

## Custom exception classes

The one case we haven’t dealt with is number 2 above: the user has entered a valid int, but the int is not in the correct range of 3, 4 or 5. As we saw above, this gives rise to an `AttributeError`, due to the `__area` attribute of the Polygon not being created. This error message isn’t very helpful to a user who has merely forgotten to enter a second number, so we’d like a better error message. Python allows us to create our own exception classes by inheriting one of the built-in ones.

For most purposes, it’s easiest to inherit the `Exception` class (rather than `BaseException`). The reason is that there are some system-generated exceptions that deal with program operation that we usually don’t want to override. The `Exception` class is a built-in class that excludes these system exceptions and deals only with exceptions generated by errors in the code. Since this is the kind of custom exception we want, we will write our own class called `PolygonError` by inheriting `Exception`.

The only feature of our new class that we want to customize is the error message that is printed. The following class definition (in PolygonError.py) does what we need:

```class PolygonError(Exception):

def __init__(self, message):
super().__init__(message)
```

We override the constructor so that we can pass a message into the class, and then user `super()` to pass this message into the base class `Exception`.

In order to use our `PolygonError`, we need to explicitly raise it in our code. We can do this by modifying the Polygon class in Polygon.py:

```from math import *
from PolygonError import *

class Polygon:
def __init__(self, numSides, sideLength) :
self.__numSides = numSides
self.__sideLength = sideLength
if numSides == 3:
self.__area = sqrt(3) * sideLength * sideLength / 4
elif numSides == 4:
self.__area = sideLength * sideLength
elif numSides == 5:
self.__area = sqrt(5 * (5 + 2*sqrt(5))) * sideLength * sideLength / 4
else:
raise PolygonError('Number of sides must be 3, 4 or 5')

def getArea(self):
return self.__area
```

After the branches of the `if` statement on lines 8 through 13, we add an `else` clause that raises a `PolygonError` with an explicit error message. The advantage of putting the `raise` statement here is that if we change the `Polygon` class later (for example, by allowing polygons with more than 5 sides to be handled), we can change the `raise` message to correspond to these changes.

We can now modify the main program so that it handles a PolygonError:

```from Polygon import *

while True:
polyData = input('Number of sides & side length: ')
if polyData == 'quit': break

polyData = polyData.split()
try:
numSides = int(polyData)
except ValueError as err:
print('Invalid quantity for number of sides:', err)
else:
try:
sideLength = float(polyData)
poly = Polygon(numSides, sideLength)
print(f'Area = {poly.getArea()}')
except ValueError as err:
print('Invalid quantity for side length:', err)
except IndexError:
print('Enter number of sides and side length (2 items)')
except PolygonError as err:
print(err)```

As the only place a `PolygonError` can be raised is when creating a `Polygon` (line 15) we add an `except` clause on line 21 to handle it. As the error message from `PolygonError` is explicit, we can just print out the exception object.

## Exercise

Write a program that implements a simple 4-function calculator. The user should be able to type in an arithmetic operation like `4.3 * -12.9` and the program should print out the answer. The input should always be in the form of three items separated by blanks, with the first and third items being floats and the middle item an operator which is one of +, -, * or /.

Write an `Arithmetic` class whose constructor takes three arguments: `left`, `operator`, `right`, where `left` and `right` are assumed to be floats and `operator` is one of the four acceptable operators.

Use exception handling to deal with the following possible errors in the user’s input:

1. The left operand is not a valid float.
2. The middle operand is not one of the four accepted operators.
3. The left operand is not a valid float.
4. The user has entered fewer than 3 items.
5. The user has attempted to divide by zero.

All the errors except #2 can be handled by catching a built-in exception. To handle #2, write your own custom exception class (note that `ArithmeticError` is already a built-in exception class, so you’ll need to call your class something different) and raise it if error #2 has occurred.

A suitable custom exception class is `ArithOpError`, in ArithOpError.py:

```class ArithOpError(Exception):

def __init__(self, message):
super().__init__(message)

```

We can use this in the `Arithmetic` class in Arithmetic.py:

```from ArithOpError import *

class Arithmetic(object):
"""does an arithmetic calculation"""

def __init__(self, left, operator, right):
if operator == '+':
self.__result = left + right
elif operator == '-':
self.__result = left - right
elif operator == '*':
self.__result = left * right
elif operator == '/':
self.__result = left / right
else:
raise ArithOpError('Operator must be one of +, -, *, /')

def getResult(self):
return self.__result```

This class performs the calculation corresponding to `operator` if that operator is one of the four acceptable ones. If it isn’t, we raise an `ArithOpError` to give some feedback to the user.

The main program is in main.py:

```from Arithmetic import *

while True:
arithOp = input('Enter an arithmetic expression: ')
if arithOp == 'quit': break

try:
arithOp = arithOp.split()
left = float(arithOp)
except ValueError:
print('Left operand must be a float')
else:
try:
right = float(arithOp)
calc = Arithmetic(left, arithOp, right)
print(f'Result = {calc.getResult()}')
except IndexError:
print('Enter (float) (operator) (float)')
except ZeroDivisionError:
print('Cannot divide by zero')
except ValueError:
print('Right operand must be a float')
except ArithOpError as err:
print(err)```

We begin by checking that the left operand is a valid float by enclosing statement 9 in the outer try block. If it isn’t, we’ll get a `ValueError` so we catch that.

If the left operand is valid, we enter the `else` block on line 12. Here we calculate the right operand and pass the three items into the `Arithmetic` constructor. There are four possible errors that can occur here.

1. There are fewer than 3 items in the `arithOp` list. This is handled by catching an `IndexError`.
2. The user has attempted to divide by zero.
3. The right operand isn’t a valid float.
4. The operator isn’t one of the four acceptable operators. This is caught in the `Arithmetic` constructor where an `ArithOpError` is raised, and then caught in the main program.

This site uses Akismet to reduce spam. Learn how your comment data is processed.