DEV Community

Cover image for Python Exceptions
Goran Vasic
Goran Vasic

Posted on

Python Exceptions

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode

Top comments (0)