Skip to content

Master Python Exception Handling with try, except and finally Blocks

Updated: at 04:23 AM

Exception handling is an important concept in Python that allows you to gracefully handle errors and unexpected situations in your code. The try, except, and finally blocks provide a robust mechanism for catching and handling exceptions in Python.

This comprehensive guide will explain the role and usage of these blocks for effective exception handling in Python. We will cover the following topics in-depth with example code snippets:

Table of Contents

Open Table of Contents

Introduction to Exceptions

An exception is an error that occurs during the execution of a program. When an exceptional condition arises, the normal flow of the program is disrupted and Python raises an exception.

Some common examples of exceptions:

If not handled properly, these exceptions crash the program and abrupt execution. To avoid disruption and make code robust, exceptions need to be caught and handled gracefully. That’s where try and except blocks come in.

The finally block also allows executing cleanup code under all circumstances.

The try and except Blocks

The try and except blocks form the basis of handling exceptions in Python. The general syntax is:

try:
   # Normal code goes here that might raise an exception
   pass

except <Exception Type>:
   # Code for handling exception
   pass

The try block contains code that might potentially raise an exception. If an exception occurs, the control jumps to the except block to handle it.

Syntax and Flow

Let’s look at a simple example that demonstrates the flow:

try:
  num = 10/0 # Raises ZeroDivisionError
except:
  print("Cannot divide by zero")

print("Code after the try-except")

Output:

Cannot divide by zero
Code after the try-except

Here, the code inside try attempts to divide by zero which raises a ZeroDivisionError. The control jumps to the except block and prints the error message. After handling, the flow resumes normally and prints the message after the try-except blocks.

If we remove the try-except, the code would simply crash on the divide by zero error.

Catching Specific Exception Types

We can catch specific exception types by specifying the exception class after except:

try:
  mylist = [1,2,3]
  print(mylist[3]) # IndexError

except IndexError:
  print("Index out of bounds")

Output:

Index out of bounds

This provides more fine-grained control instead of generically catching all exceptions. We can handle different exceptions with different actions.

Catching All Exceptions

We can use except Exception to catch all types of exceptions:

try:
   risky_code()

except Exception:
   # Handles all exceptions
   print("Unknown exception occurred")

This can be used when we want to generically handle all errors in a section of code.

Accessing the Exception Object

When an exception occurs, Python creates an exception object containing useful information. We can access it in the except block by providing a variable after the exception type:

try:
  num = 10/0
except ZeroDivisionError as e:
  # e contains exception object
  print(e)
  print(e.args)

Output:

division by zero
('division by zero',)

We can access the exception message, type, traceback and other attributes from this object.

The else Block

The else block allows executing code when no exceptions occur within the try block:

try:
   num = 10/2
   print(num)
except ZeroDivisionError:
   print("Divide by zero error")
else:
   print("No exceptions raised")

Output:

5.0
No exceptions raised

Here, since no exception occurred in the try, the else block was executed after the try.

The main use case is to place code that must execute only when no exceptions occur in the try block.

The finally Block

The finally block always executes after the try and except, even if an unhandled exception causes the program to terminate:

try:
   risky_code()

except:
   # Exception handling

finally:
   # Always executes
   print("Cleaning up")

This allows us to define cleanup actions such as closing files, connections, etc that should always run regardless of any exceptions.

finally improves the overall robustness of programs by ensuring critical clean up code gets executed.

Best Practices for Exception Handling

Here are some key best practices to follow for exception handling in Python:

Following these practices allow robust and maintainable error handling.

Chaining Multiple Exceptions

We can chain multiple except blocks to handle different specific exceptions in different ways:

try:
  risky_code()

except ValueError:
  # Handle ValueError

except IOError:
  # Handle IOError

except Exception as e:
  # Handle any other exceptions
  print("Unknown exception occurred")

The exception propagation follows top to bottom order. The first match catches and handles the exception.

We can also chain except blocks without exception types to create a logical handling flow:

try:
   pass

except:
   # Some handling

except:
   # Some other handling

Raising Exceptions

We can manually raise exceptions using the raise statement by providing the exception object:

# Raise TypeError
raise TypeError("Invalid type")

Common cases where manually raising exceptions is required:

We can pass an exception message to the constructor which gets printed in the traceback.

Creating Custom Exceptions

Python also allows creating custom exception classes by subclassing the inbuilt ones:

class CustomError(Exception):
  pass

raise CustomError("Custom exception")

This allows more specialized exceptions representing specific error conditions relevant to an application.

Key points for custom exceptions:

Real-World Examples

Let’s look at some real-world use cases of exception handling in Python.

Handling Divide by Zero Errors

Performing a division is a common operation where we need to watch out for potential divide by zero errors:

def safe_division(a, b):
  try:
    result = a/b
  except ZeroDivisionError:
    print("Divide by 0 error. Setting result to 0")
    result = 0

  return result

print(safe_division(10, 0))
# Prints: Divide by 0 error. Setting result to 0
# Returns: 0

Here we catch the specific ZeroDivisionError and handle it by returning a safe default value.

Handling Missing File Errors

When working with files, we need to handle cases where the file may not exist:

import os

def read_file(filename):
  try:
    with open(filename) as f:
      return f.read()

  except FileNotFoundError:
    print(f"File {filename} does not exist")
    return None

print(read_file("non_existing.txt"))
# Prints: File non_existing.txt does not exist
# Returns: None

Catching FileNotFoundError allows us to notify about missing files instead of a cryptic exception.

Handling Invalid User Input

For user-facing programs, we need to validate data and handle invalid input gracefully:

def process_input(user_input):
  try:
    # Convert to int
    num = int(user_input)

  except ValueError:
    # Handling invalid casting
    print(f"{user_input} is not a valid integer")
    return None

  # Rest of processing
  return num * 2

print(process_input("10"))
# Prints: 20

print(process_input("foo"))
# Prints: foo is not a valid integer

Here we catch ValueError during type casting and print a cleaner error message.

Conclusion

The try, except, and finally blocks provide a robust mechanism for handling errors and exceptional conditions in Python code. Using them appropriately makes programs resilient and production-ready by turning crashes into graceful exits.

Following the best practices outlined ensures clean, well-structured error handling. Chaining multiple exceptions allows handling various errors differently. Raising and creating custom exceptions helps model real-world error scenarios for cleaner code.

Overall, learning effective exception handling is an important Python programming skill for beginners and experts alike. Mastering these techniques will enable you to write stable, robust programs ready for the real world.