What is an Exception?¶
Exceptions are like some irritating friends that you will meet one way or another in your Python journey. Anytime you try to do something unruly or ask Python to do something that Python doesn't like, you will likely be handed over with some Exceptions that will stop or just warn you depending on the level of "offense".
It's Python's way to tell you that what you asked for is either not achievable or needs something to take care of to avoid future execution failure.
In this post, we will go into the details of Python Exceptions. We will learn how to handle them, understand them as Python classes, how to customise our responses based on the type of exceptions, and will also know how to create own own exceptions.
Since Exceptions are classes themselves, we will use a lot of references to Python classes. So in case you need a quick brush up, you can my blog post series on Object Oriented Programming in Python.
Handling Exceptions¶
Exceptions are Python's way of telling you something didn't go as planned or expected. In an interactive and ad-hoc coding scenario, such as data analysis, we typically don't need to take care of Exceptions since we fix the exceptions as we face them and move on. But if you want to put your script for any kind of automated task, such as an ETL job, leaving the exceptions unhandled is pretty much like leaving a loaded gun unattended and aimed at your feet. You never know when someone will give it a jolt and inadvertently shoot it!
By handling exception we basically mean setting up a set of commands so that if an Exception takes place, Python knows what to do next other than just throwing a fit.
The Simplest Way¶
To capture the exceptions we use a code block called try-except
. We put the piece of code that is suspected to be a source of error inside a try
block then capture and design the response inside the except
block. Check the following example where function calcArea
is defined to receive a value from the users for theradius
of a circle and returns the calcualated area of the circle.
def calcArea(radius):
pi = 3.1416
radius = float(radius)
area = pi * radius ** 2
return area
for radius in [5, "a", 4, 8, "b", 0]:
try:
area = calcArea(radius)
print("Area for radius {} = {}\n".format(radius, area))
except Exception as e:
print("Something is wrong with {}\n".format(radius))
Area for radius 5 = 78.53999999999999 Something is wrong with a Area for radius 4 = 50.2656 Area for radius 8 = 201.0624 Something is wrong with b Area for radius 0 = 0.0
In the for
loop,
Python first executes the
calcArea()
inside thetry
block.- It tries to calculate the area and save it to the variable called
area
. - If it doesn't encounter any exception, it prints out the newly created variable
area
. - But if it encounters a problem, it moves to the
except
code block.
- It tries to calculate the area and save it to the variable called
Inside
except
code block,- The
Exception
class is captured or stored and given a name -e
, or give it any name of your wish. This named object is then used later to access the elements of the exception class object.
- The
But what's the benefit of exception handling?
Notice, how the for
loop didn't break once it got unexpected values i.e. strings
. If we didn't handle the Exceptions thrown from calcArea()
, the for
loop would have broken right after executing the first element.
🛑 Give it a try by to see yourself! Try running calcArea()
in the loop without using try-except
.
Let's be Slightly More Engaged¶
The try-except
code block has two additional branches: else
, and finally
.
else
code block is executed only if no exception takes place. We can use it to print out a custom message for a successful operation in a cleaner way - other than cramping it inside thetry
block.finally
code block is always executed irrespective of there is an exception or not. Commonly used to leave a footprint to flag the end of the opration.
from datetime import datetime
for radius in [5, "a", 4, 8, "b", 0]:
try:
area = calcArea(radius)
except Exception as e:
area = None
print("Something is wrong for {}.".format(radius))
else:
print("calcArea() ran successfully for {}".format(radius))
finally:
now = datetime.now()
print("Area for input {} = {} \ncalcArea() run completed on {}\n".format(radius, area, now))
calcArea() ran successfully for 5 Area for input 5 = 78.53999999999999 calcArea() run completed on 2022-03-24 10:36:49.778314 Something is wrong for a. Area for input a = None calcArea() run completed on 2022-03-24 10:36:49.778314 calcArea() ran successfully for 4 Area for input 4 = 50.2656 calcArea() run completed on 2022-03-24 10:36:49.779315 calcArea() ran successfully for 8 Area for input 8 = 201.0624 calcArea() run completed on 2022-03-24 10:36:49.779315 Something is wrong for b. Area for input b = None calcArea() run completed on 2022-03-24 10:36:49.779315 calcArea() ran successfully for 0 Area for input 0 = 0.0 calcArea() run completed on 2022-03-24 10:36:49.779315
How About Being More Expressive!¶
Our try-except
block tells us there's something wrong but doesn't tell us what exactly went wrong. In this section, we will work on that.
As we already know that exceptions in Python are basically classes themselves, they also come with some built-in variables of a Python class. In the following example, we will use some of these class properties to make the error messages more expressive.
Let's look at the examples first then we will come back to discuss more about the class properties used in the example.
from datetime import datetime
for radius in [5, "a", 4, 8, "b", 0]:
try:
area = calcArea(radius)
except Exception as e:
area = None
error_type = type(e).__name__
print("Area couldn't be calculated for {}.".format(radius))
print("Error type: {} \nError msg: {}.".
format(error_type, e))
else:
print("calcArea() ran successfully for {}".format(radius))
finally:
now = datetime.now()
print("Area for input {} = {} \ncalcArea() run completed on {}\n".format(radius, area, now))
calcArea() ran successfully for 5 Area for input 5 = 78.53999999999999 calcArea() run completed on 2022-03-24 10:36:52.476687 Area couldn't be calculated for a. Error type: ValueError Error msg: could not convert string to float: 'a'. Area for input a = None calcArea() run completed on 2022-03-24 10:36:52.476687 calcArea() ran successfully for 4 Area for input 4 = 50.2656 calcArea() run completed on 2022-03-24 10:36:52.476687 calcArea() ran successfully for 8 Area for input 8 = 201.0624 calcArea() run completed on 2022-03-24 10:36:52.476687 Area couldn't be calculated for b. Error type: ValueError Error msg: could not convert string to float: 'b'. Area for input b = None calcArea() run completed on 2022-03-24 10:36:52.477706 calcArea() ran successfully for 0 Area for input 0 = 0.0 calcArea() run completed on 2022-03-24 10:36:52.477706
type(e).__name__
: Prints out the subclass name of the Exception.except
block prints outTypeError
for when it encountersstring
as input.print(e)
: Prints out the entire error message.
But if e
is an object of the Exception class, shouldn't we use something like e.print_something()
?
It's possible because the Python Exception classes come with a built-in method called __str__()
in them. Having this method defined in a class makes it possible to directly print out a class.
🛑 Give it a try! Rather than calling e
, call e.__str__()
to print out the error message.
What if We Needed Some Customization!¶
From our calcArea()
function, we can see that ValueError
is a repeated error type - where users input a string
rather than a numeric value and thus Python fails to perform a numeric operation. How about if we customize try-except
block to be more accommodative to this common error type and give the users another chance before ignoring their input entirely?
To do that we can call out a separate code block specifically for the ValueError
exception. Where we will check or validate the input type
to ensure it's only int
or float
type. Otherwise, we will keep prompting the user to input a numeric value as input.
To make the validation process simpler, let's define a function called validate_input()
- it'll check, and return True
if the input variable is a float
type otherwise return False
status.
def validate_input(value):
try:
value = float(value)
return True
except: return False
from datetime import datetime
for radius in [5, "a", 4, 8, "b", 0]:
try:
area = calcArea(radius)
except ValueError:
print("Input data is not numeric type.")
while not validate_input(radius):
radius = input("Please input a numeric value: ")
area = calcArea(radius)
except Exception as e:
area = None
error_type = type(e).__name__
print("Area couldn't be calculated for {}.".format(radius))
print("Error type: {} \nError msg: {}.".
format(error_type, e))
else:
print("calcArea() ran successfully for {}".format(radius))
finally:
now = datetime.now()
print("Area for input {} = {} \ncalcArea() run completed on {}\n".format(radius, area, now))
calcArea() ran successfully for 5 Area for input 5 = 78.53999999999999 calcArea() run completed on 2022-03-24 10:36:57.001287 Input data is not numeric type. Please input a numeric value: 8 Area for input 8 = 201.0624 calcArea() run completed on 2022-03-24 10:37:00.426775 calcArea() ran successfully for 4 Area for input 4 = 50.2656 calcArea() run completed on 2022-03-24 10:37:00.426775 calcArea() ran successfully for 8 Area for input 8 = 201.0624 calcArea() run completed on 2022-03-24 10:37:00.426775 Input data is not numeric type. Please input a numeric value: k Please input a numeric value: 999 Area for input 999 = 3135319.9416 calcArea() run completed on 2022-03-24 10:37:04.927187 calcArea() ran successfully for 0 Area for input 0 = 0.0 calcArea() run completed on 2022-03-24 10:37:04.927187
To custom response for ValueError
, we have added a dedicated code block to capture only ValueError
exception. Inside that code exception block,
- We are printing a message to the users.
- Then we are requesting for a numeric input in a loop until the input type is numeric.
- And finally upon receiving a numeric type input we call
calcArea()
again.
Notice a couple of things,
📌 We have put the ValueError
code block above the Exception
block. If we did otherwise our code would never have reached the ValueError
section. Since it's a subclass of the Exception class, it would have been captured by the Exception
code block.
📌 We didn't use ValueError as e
in ValueError
code block but we could have. Since we didn't need any class properties to print out or for other use, we didn't capture TypeError class as an object.
🛑 Think about a scenario where you may want to incorporate other specific exception types. How would you incorporate them?
Understanding Exceptions as a Class¶
I have mentioned several times in this post that Exceptions are classes but haven't got much detail into that rather than showing some applications. Let's try to do that in this section.
In Python, all the Exceptions derive from the class called
BaseException
.
Below BaseException
all the built-in exceptions are arranged as a hierarchy. The main four subclasses under BaseException
are:
- SystemExit
- KeyboardInterrupt
- GeneratorExit
- Exception
The exceptions that we commonly care about or want to take action upon are the ones under the Exception subclass. Exception subclass again contains several groups of subclasses. Following is a partial tree hierarchy of the Python classes. For detail refer to this official document from python.org.
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
| +-- ModuleNotFoundError
+-- LookupError
🛑 You can run methods used to check common class hierarchy relations on these classes to check the above hierarchy yourself. Try running,
print(BaseException.__subclasses__())
: you should see the main subclasses under theBaseException
class.print(issubclass(ValueError, Exception))
: you should seeTrue
as return value sinceValueError
is a subclass ofException
.
Implementing Our Own Exception¶
So far we have been using only the exceptions that are already defined by Python. What if we want to have a further customized exception? To do that we can define our own exception class.
Since Exceptions are class, we can write our own custom exception class the same way we write a regular Python class.
In our example, let's assume that we want to restrict the user input to a manageable size so that a user can't ask the program to calculate area for an arbitrarily large radius. For demonstration purposes, let's say we want to restrict it within 50
inches.
We can do that easily by adding an if
condition inside the calcArea()
function. But there's a nicer way of doing it using exceptions
.
Writing a Custom Exception¶
For our solution, we will first define our own exception class called Above50Error
.
class Above50Error(Exception):
def __init__(self, value):
Exception.__init__(self)
self.value = value
def __str__(self):
return "Input {} is larger than 50 inches".format(self.value)
Above50Error
exception class is created as a subclass of the built-in Exception class so that it inherits all the properties.
- We initiated it with one parameter:
value
which is then stored as a class variable calledvalue
. - Then we overrode the
__str__()
method that's inherited from the Exception class to print out a custom message.
Note that to print out a custom message we could've just added a message as a parameter while initializing Above50Error
and pass it to Exception
initialization. Exception
class would have taken the argument passed as a message and customize the __str__()
method. But we implemented it the longer way only for demo purposes.
🛑 Give it a try! Modify Above50Error
so that it doesn't need the __str__()
method to print a message.
🛑 Also, can you think of a way to confirm if Above50Error
is actually a subclass of Exception
?
To learn more about Python class and subclass relation you can check out my post on Object Oriented Programming in Python - Inheritance and Subclass
Implementing a Custom Exception¶
Since Above50Error
is a custom exception, we need to push Python to raise it as an exception when we want to register this as an exception. Thus when we put that code inside a try-except
code block it can capture the behavior as an exception. To do that we use the raise
keyword.
So we modified calcArea()
function to add a condition that checks if the input value is higher than 50. If it is it raises Above50Error
exception otherwise it moves on and calculates the areas.
def calcArea(radius):
pi = 3.1416
radius = float(radius)
if radius > 50:
raise Above50Error(radius)
else:
area = pi * radius ** 2
return area
from datetime import datetime
for radius in [5, "5", 55, 4, 8, "b", 0]:
try:
area = calcArea(radius)
except ValueError:
print("Input data is not numeric type.")
while not validate_input(radius):
radius = input("Please input a numeric value: ")
area = calcArea(radius)
except Exception as e:
area = None
error_type = type(e).__name__
print("Area couldn't be calculated for {}.".format(radius))
print("Error type: {} \nError msg: {}.".
format(error_type, e))
else:
print("calcArea() ran successfully for {}".format(radius))
finally:
now = datetime.now()
print("Area for input {} = {} \ncalcArea() run completed on {}\n".format(radius, area, now))
calcArea() ran successfully for 5 Area for input 5 = 78.53999999999999 calcArea() run completed on 2022-03-24 10:37:15.619721 calcArea() ran successfully for 5 Area for input 5 = 78.53999999999999 calcArea() run completed on 2022-03-24 10:37:15.619721 Area couldn't be calculated for 55. Error type: Above50Error Error msg: Input 55.0 is larger than 50 inches. Area for input 55 = None calcArea() run completed on 2022-03-24 10:37:15.619721 calcArea() ran successfully for 4 Area for input 4 = 50.2656 calcArea() run completed on 2022-03-24 10:37:15.619721 calcArea() ran successfully for 8 Area for input 8 = 201.0624 calcArea() run completed on 2022-03-24 10:37:15.619721 Input data is not numeric type. Please input a numeric value: k Please input a numeric value: 7 Area for input 7 = 153.9384 calcArea() run completed on 2022-03-24 10:37:20.308639 calcArea() ran successfully for 0 Area for input 0 = 0.0 calcArea() run completed on 2022-03-24 10:37:20.308639
Notice how the except code block for Exception
captures our customed exception. Before wrapping up let's highlight some features of a custom exception class:
- Custom exception classes are like regular built-in exception classes. For example, we could have created a separate except code block for
Above50Error
as we did forValueError
. Give it a try! - Also, since these are classes we could create their own subclasses to have even more customization on error and exceptions.
What's Next?¶
This will be a wrap on my Object-Oriented Programming in Python series. In this series I tried to explain:
- What is OOP and why you should care about it?
- Understanding a class.
- Understanding inheritance and application of this concept in the subclasses.
- Understanding Python variables in the class context.
- Understanding Python methods.
And finally, in this blog, we went deep into the Python exceptions as an example of Python class.
Thanks for reading the post and hopefully this blog series will give you a good initial understanding of Object Oriented Programming in Python. In a future series, we will go on another journey into the world of Python class and have a much deeper understanding.