1. python
  2. /advanced
  3. /exception-handling

Guide to Exception Handling in Python

Getting Started

As Python programmers, we'll inevitably stumble upon errors. Roughly, there are two types of errors; syntax errors and exceptions. The first stem from syntax blunders or other oversights in our code, while exceptions are those pesky messages that pop up during program execution.

For us to handle these mishaps and ensure the program runs smoothly, we'll need to include exception handling in our toolkit.

Syntax Errors

Sometimes, we refer to syntax errors as parsing ones and they arise when the Python interpreter stumbles upon a line of code that it can't make sense of. These blunders often result from typos, incorrect syntax, or other mistakes.

print("Hello, world!')

# SyntaxError: unterminated string literal (detected at line 1)

So, the syntax error here is because we didn't match the quotations type properly. While it may seem trivial, a tiny mistake such as misspelling or missing symbols can bring our code to a halt.

Logical Errors

Logical errors are subtle and may slip through when our code runs without any syntax or runtime errors, yet the output isn't quite what we anticipated. Since they don't cause your program to crash or raise an exception, logical errors can be notoriously elusive. Take a look at the following example:

def percentage(part, total):
    return part / total * 100

score = 45
max_score = 50

result = percentage(max_score, score)
print(f"The percentage is {result}%")

# Output: The percentage is 111.11111111111111%

In this case, the logical error lies in the incorrect order of arguments when we call the percentage function. We should be passing score as the first argument and max_score as the second. Instead, we have them reversed, leading to an inaccurate percentage calculation.

Runtime Errors

Runtime exceptions crop up during program execution when something unforeseen goes wrong. These issues aren't detectable by the Python interpreter during the compilation phase and can be caused by a variety of factors, such as invalid input data or network failures.

num1 = 10
num2 = 0

result = num1 / num2

print(result)

# ZeroDivisionError: division by zero

So, here the error is raised because we're attempting to divide by zero, which is mathematically undefined. Python catches and informs us about this type of runtime exception during the program execution.

The Try-except Block

Now, let's focus on handling exceptions using the try-except block. With this approach, we can catch exceptions and address them in an orderly fashion.

try:
    # Code that might raise an exception
except ExceptionType:
    # Code to handle the exception

First, we place the code that might raise an exception within the try block. Then, in the accompanying except block, we can specify the type of exception we want to catch, along with the code to handle that particular exception.

try:
    num1 = int(input("Enter a number: "))
    num2 = int(input("Enter another number: "))

    result = num1 / num2

    print(result)
except ZeroDivisionError:
    print("You can't divide by zero!")
except ValueError:
    print("Invalid input. Please enter a valid integer.")

The code inside the try block prompts the user to input two numbers, performs a division operation, and displays the result. If our user enters a zero for the second number, a ZeroDivisionError is triggered. Or, if he enters a non-numeric value, a ValueError shows up. The code within the except blocks neatly captures these exceptions and prints suitable error messages for them.

Keep in mind that we can use multiple except blocks to handle various types of exceptions. If an exception arises that doesn't align with any of the designated except blocks, the program will terminate, accompanied by an error message we provide.

The Finally Block

The finally block serves a crucial purpose in exception handling — we can ensure that specific code segments execute, irrespective of whether an exception is raised. Note that this feature can be particularly handy when we need to close files, release resources, or perform any necessary cleanup tasks.

try:
    # Code that might raise an exception
except ExceptionType:
    # Code to handle the exception
finally:
    # Code to execute regardless of whether an exception was raised

Let's see it in action.

try:
    file = open("data.txt", "r")
    data = file.read()
    print(data)
except FileNotFoundError:
    print("File not found.")
finally:
    file.close()

So, in the try block, we aim to open a file, read its contents, and then display them. If the file is nowhere to be found, a FileNotFoundError is triggered. Despite the presence or absence of an exception, the finally block ensures that the file is closed, maintaining a clean, orderly program execution.

Raising Exceptions

Beyond handling exceptions raised by Python itself, we can create and raise our own with the raise statement.

raise ExceptionType("Error message")

Let's explore this concept with a different example.

def greet(name):
    if not name:
        raise ValueError("Name cannot be empty.")
    return f"Hello, {name}!"

try:
    message = greet("")
except ValueError as error:
    print(error)

Initially, the greet function raises a ValueError if the provided name is empty. Below, we can observe the code within the try block that calls the greet function with an empty string, triggering the exception.

Lastly, the except block catches the exception and prints the corresponding error message. By raising custom exceptions we can better handle specific scenarios and maintain control over the execution flow.

Custom Exceptions

There may be occasions when you need to create your exceptions to manage specific error conditions in your code. This can be achieved by defining a new class that inherits from the Exception class.

class CustomException(Exception):
    pass

Let's examine a practical example.

class InvalidInputError(Exception):
    def __init__(self, message):
        self.message = message

try:
    age = int(input("Enter your age: "))
    if age < 0:
        raise InvalidInputError("Age cannot be negative.")
except InvalidInputError as error:
    print(error.message)

We defined the InvalidInputError class to tackle situations where users input a negative age. The try block prompts the user to enter their age; if a negative age is provided, it raises an instance of the class. Moreover, the except block subsequently catches the exception and displays the associated error message. Logically, this demonstrates how custom exceptions can help us address unique error scenarios and maintain greater control over the execution.

Final Thoughts

Undoubtedly, exception handling can be a difference maker. It enables us to manage errors and maintain smooth program execution, even when unexpected issues arise.

We explored various types of exceptions in Python and discussed handling them using try-except blocks, utilizing the finally block, raising exceptions, and even creating custom ones.

Don't forget to check out the useful resources below to expand your knowledge and strengthen your Python skills. And remember, it never hurts to revisit some of the basic Python concepts included in the examples throughout this article.

Useful Resources

Offical Python Documentation on Errors and Exceptions

Python's Built-in Exceptions

The Basics of Python Functions

The Basics of Python Data Types