An Exception
is a special type of object in Python, and Python has many built-in exception types. All exceptions must be instances of a class that derives from BaseException, which means that Python exceptions form a hierarchy.
To raise an exception, means to start an exception event flow. Exception handling is interacting with an exception flow in some manner. An unhandled exception is an exception flow that is not handled by your code, and generally it means that your program will terminate abruptly.
There are several built-in functions you can use to inspect the exceptions:
# Create an exception object
ex = ValueError("An unexpected value has been provided.")
# Inspect an exception
type(ex) # -> ValueError
print(ex) # -> An unexpected value has been provided.
repr(ex) # -> ValueError("An unexpected value has been provided.")
# Raise an exception
raise ex
LBYL vs EAFP
Something that is "exceptional" should be infrequent. If we are dividing two integers in a loop that repeats 1.000 times, and if, out of every 1.000 we run, we expect division by zero to occur 5 times, we might have 2 approaches:
- LBYL (Look Before You Leap) – Test that divisor is non-zero 1.000 times.
- EAFP (Easier to Ask Forgiveness than Permission) – Just do it, and handle the division by zero error 5 times (this is often more efficient).
Also, trying to fully determine if something is going to go wrong is a lot harder to write than just handling things when they do go wrong.
Read the Look Before You Leap article for more info on LBYL and EAFP approaches for exceptions.
Optimizing Performance with Exceptions
In some cases, relying on exceptions can help you improve your code performance. Consider the following Python code:
from timeit import timeit
def process():
l = list(range(1_000))
while len(l) > 0: # Calling the len() function in each iteration
l.pop()
timeit("process()", globals=globals(), number=500_000) # 40.69522000011057
The issue with this code is that it checks the length of the list by calling Python's built-in function len()
at every iteration.
A significant performance improvement can be achieved by using the for
loop, which checks the length of the list only once:
from timeit import timeit
def process():
l = list(range(1_000))
for i in range(len(l)): # Calling the len() function once
l.pop()
timeit("process()", globals=globals(), number=500_000) # 25.074866899987683
In this case, the code executes almost 50% faster (25s compared to previous 40s). The issue here is that the process()
function is again being called 500.000
times by the timeit()
function, and the process()
function still invokes the len()
built-in function at every iteration.
A better solution turns out to be using the try-except
block, where you simply ignore the IndexError
exception that gets raised if the pop()
method is called on an empty list:
from timeit import timeit
def process():
try:
l = list(range(1_000))
while True:
l.pop()
except IndexError:
... # Ignore the exception
timeit("process()", globals=globals(), number=500_000) # 18.16189290001057
With this approach the same code takes only 18 seconds to execute. It is worth mentioning that you should always target a specific exception – in this case IndexError
, not Exception
, which is too broad.
Using Your Own Exceptions
In Python you can define your own custom exceptions:
# Define a custom exception class
class MyCustomException(Exception):
def __init__(self, message="A custom exception occurred"):
self.message = message
super().__init__(self.message)
# Check if MyCustomException is a subclass of Exception
print(issubclass(MyCustomException, Exception)) # True
# Example usage of the custom exception:
try:
raise MyCustomException
except MyCustomException as ex:
print(f"Caught exception #1: {ex}") # Caught exception #1: A custom exception occurred
# Example usage of the custom exception with different error message:
try:
raise MyCustomException("This is a custom exception")
except MyCustomException as ex:
print(f"Caught exception #2: {ex}") # Caught exception #2: This is a custom exception
try-except-else-finally
In combination with the try-except
block, Python's syntax allows you to use the else
clause that executes only if the try-except
block was successful, and the finally
clause that is always executed, so you can use it to ensure that certain tasks are performed if your application terminates abruptly:
# No exceptions raised
x = 4
y = 2
try:
z = x / y
except ZeroDivisionError:
raise
else:
print(f"ELSE: {x} / {y} = {z}")
finally:
print("FINALLY: Perform some tasks...")
# ELSE: 4 / 2 = 2.0
# FINALLY: Perform some tasks...
# Dividing by zero raises ZeroDivisionError
x = 4
y = 0
try:
z = x / y
except ZeroDivisionError:
raise
else:
print(f"{x} / {y} = {z}")
finally:
print("Perform some tasks...")
# FINALLY: Perform some tasks...
# ---------------------------------------------------------------------------
# ZeroDivisionError Traceback (most recent call last)
# Cell In[9], line 5
# 2 y = 0
# 4 try:
# ----> 5 z = x / y
# 6 except ZeroDivisionError:
# 7 raise
#
# ZeroDivisionError: division by zero
Top comments (0)