Mistakes and errors happen in computer programs as much as in real life. Like life, how you handle an error in your program shows your level of professionalism, and gives others evidence that they can trust that you have written a program that will work well.
In the last section we indicated errors in the Person.setHeight
function by printing a message to the screen and returning False
to indicate that the call to setHeight
had failed.
class Person:
"""Class that holds a person's height"""
def __init__(self):
"""Construct a person who has zero height"""
self._height = 0
def setHeight(self, height):
"""Set the person's height to 'height', returning whether or
not the height was set successfully
"""
if height < 0 or height > 300:
print("This is an invalid height! %s" % height)
return False
else:
self._height = height
return True
def getHeight(self):
"""Return the person's height"""
return self._height
p = Person()
p.setHeight(-20)
This is not a good way of indicating an error. The issues with this are;
getHeight
know to check whether the call returns True
or False
The solution is to send something to the programmer that they cannot ignore, which indicates that there is an error. That something is called an "exception".
Take a look at this simple code that sets the height...
def setHeight(height):
if height < 0 or height > 300:
raise ValueError("Invalid height: %s. This should be between 0 and 300" % height)
print("Height is set to %s" % height)
setHeight(-5)
When we try to use an invalid value for the height, we raise (or throw) a ValueError
exception. This stops the function from continuing, and gives us a very helpful print out of what went wrong, and where.
ValueError
is just a class. The name of the class provides us with useful information (there was an error with a value in the program). You choose what error you want to raise. Python provides a set of usefully named error classes that you can use:
IOError
: Error raised when you have a problem with IO, e.g. opening or closing filesZeroDivisionError
: Error raised when you divide by zeroTypeError
: Error raised when you are using the wrong type, e.g. maybe setting the height to a stringIndexError
: Error raised when you are using an invalid index to access a list or other similar containerKeyError
: Error raised when you are using an invalid key to access a dictionary or other similar containerA full list of standard Python exceptions is available here.
You are free to raise any exception class you want. It is your job as a programmer to choose the one that is most sensible, e.g.
def setHeight(height):
if height < 0 or height > 300:
raise ZeroDivisionError("Invalid height: %s. This should be between 0 and 300" % height)
print("Height is set to %s" % height)
setHeight(400)
Using a ZeroDivisionError
is a bad choice, as the error has nothing to do with division by zero. A ValueError
is the right choice as the error relates to an invalid value passed to the function.
You are free to create your own exception classes.
class InvalidHeightError(Exception):
pass
def setHeight(height):
if height < 0 or height > 300:
raise InvalidHeightError("Invalid height: %s. This should be between 0 and 300" % height)
print("Height is set to %s" % height)
setHeight(-10)
Your own exception classes must be declared as derived from type Exception
, hence why you have to write class InvalidHeightError(Exception):
. As the class doesn't need to do anything else, you can use pass
to say that nothing else needs to be added. Note that you can call your error class anything you want. By convention, it is good to end the class name with Error
so that other programmers know what it is for.
Here is an extended copy of the Person
code from above.
class Person:
"""Class that holds a person's height"""
def __init__(self, height=0, weight=0):
"""Construct a person with the specified name, height and weight"""
self.setHeight(height)
self.setWeight(weight)
def setHeight(self, height):
"""Set the person's height in meters"""
if height < 0 or height > 2.5:
raise ValueError("Invalid height: %s. This shoud be between 0 and 2.5 meters" % height)
self._height = height
def setWeight(self, weight):
"""Set the person's weight in kilograms"""
if weight < 0 or weight > 500:
raise ValueError("Invalid weight: %s. This should be between 0 and 500 kilograms" % weight)
self._weight = weight
def getHeight(self):
"""Return the person's height in meters"""
return self._height
def getWeight(self):
"""Return the person's weight in kilograms"""
return self._weight
def bmi(self):
"""Return the person's body mass index (bmi)"""
return self.getWeight() / self.getHeight()**2
Edit the above copy of Person
to ensure that the .setWeight
function only accepts valid weights. A valid weight is any number that is between 0 and 500 kilograms. You should raise a ValueError
if the weight is outside this range. For the moment, do not worry about the user supplying a non-numeric weight.
Also edit the above copy of Person
to ensure that the .setHeight
function only accepts valid heights. A valid height is any number that is between 0 and 2.5 meters. You should raise a ValueError
if the height is outside this range. For the moment, do not worry about the user supplying a non-numeric height.
Check that a ValueError
exception is correctly raised if invalid heights or weights are supplied. Also check that the ValueError
exception is not raised if a valid height and weight are supplied.
p = Person(height=2.8, weight=500)
p = Person(height=1.8, weight=501)
p = Person(height=1.8, weight=150)
If you run the following code;
p = Person()
p.bmi()
it will raise a DivideByZero
exception. This is because the calculation involves dividing by the height squared, which is zero in a default-constructed Person
. While an exception has been raised, it is not very intuitive for another programmer to debug. A solution is to create your own named exception that provides more information.
Create a new exception called NullPersonError
, and edit the .bmi()
function so that this exception is raised if it is called on a Person
whose height or weight is zero.
Check that the NullPersonError
exception is raised if .bmi()
is called on a default-constructed Person
. Check that this exception is not raised if .bmi()
is called on a properly constructed Person
.
class NullPersonError(Exception):
pass
class Person:
"""Class that holds a person's height"""
def __init__(self, height=0, weight=0):
"""Construct a person with the specified name, height and weight"""
self.setHeight(height)
self.setWeight(weight)
def setHeight(self, height):
"""Set the person's height in meters"""
if height < 0 or height > 2.5:
raise ValueError("Invalid height: %s. This shoud be between 0 and 2.5 meters" % height)
self._height = height
def setWeight(self, weight):
"""Set the person's weight in kilograms"""
if weight < 0 or weight > 500:
raise ValueError("Invalid weight: %s. This should be between 0 and 500 kilograms" % weight)
self._weight = weight
def getHeight(self):
"""Return the person's height in meters"""
return self._height
def getWeight(self):
"""Return the person's weight in kilograms"""
return self._weight
def bmi(self):
"""Return the person's body mass index (bmi)"""
if (self.getHeight() == 0 or self.getWeight() == 0):
raise NullPersonError("Cannot calculate the BMI of a person with zero "
"height or weight (%s,%s)" % (self.getHeight(),self.getWeight()))
return self.getWeight() / self.getHeight()**2
p = Person()
p.bmi()
p = Person(height=1.8, weight=77.5)
p.bmi()