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[0]) sideLength = float(polyData[1]) 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.
- The first entry is not a valid int.
- The first entry is a valid int, but is not 3, 4 or 5.
- The second entry is not a valid float.
- 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:
- 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'”. - 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 ifnumSides
isn’t 3, 4 or 5, the__area
attribute is never calculated, so it doesn’t exist. - 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'”. - 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 expectpolyData
to have 2 elements with indexes [0] and [1], and we have only the [0] 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[0]) except ValueError as err: print('Invalid quantity for number of sides:', err) else: try: sideLength = float(polyData[1]) 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[0]) except ValueError as err: print('Invalid quantity for number of sides:', err) else: try: sideLength = float(polyData[1]) 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:
- The left operand is not a valid float.
- The middle operand is not one of the four accepted operators.
- The left operand is not a valid float.
- The user has entered fewer than 3 items.
- 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[0]) except ValueError: print('Left operand must be a float') else: try: right = float(arithOp[2]) calc = Arithmetic(left, arithOp[1], 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.
- There are fewer than 3 items in the
arithOp
list. This is handled by catching anIndexError
. - The user has attempted to divide by zero.
- The right operand isn’t a valid float.
- The operator isn’t one of the four acceptable operators. This is caught in the
Arithmetic
constructor where anArithOpError
is raised, and then caught in the main program.