Exception Handling Python: Try-Except Mastery

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

Python try except blocks prevent your programs from crashing when errors occur. The code above catches a division by zero error and handles it gracefully instead of terminating the program. This mechanism separates normal execution flow from error handling logic, making your code more reliable and maintainable.

How python try except blocks work

Python executes the code inside the try block first. If no error occurs, the except block gets skipped entirely. When an exception happens, Python immediately stops executing the try block and jumps to the matching except handler.

try:
    user_input = input("Enter a number: ")
    number = int(user_input)
    print(f"You entered: {number}")
except ValueError:
    print("That's not a valid number")

The flow starts with requesting user input. Python then attempts to convert the input to an integer. If the user types “hello” instead of a number, the int() function raises a ValueError. The except block catches this specific error and displays a helpful message.

Catching multiple exception types in python

Real programs need to handle different error scenarios. You can specify multiple except blocks to catch distinct exception types.

try:
    file = open("data.txt", "r")
    content = file.read()
    value = int(content)
    file.close()
except FileNotFoundError:
    print("File doesn't exist")
except ValueError:
    print("File contains invalid data")
except PermissionError:
    print("Don't have permission to read file")

Each except block targets a specific error type. When Python encounters an exception, it checks each except clause in order. The first matching handler executes, and the rest get ignored. This ordering matters when dealing with exception hierarchies.

You can also catch multiple exceptions in a single except block by using a tuple.

try:
    data = fetch_api_data()
    process_data(data)
except (ConnectionError, TimeoutError):
    print("Network request failed")

This approach works when you want identical handling for related exceptions. Both ConnectionError and TimeoutError trigger the same response.

Accessing exception details with python try except

Exception objects carry information about what went wrong. You can capture this data using the as keyword.

try:
    result = calculate_average([1, 2, "three", 4])
except TypeError as error:
    print(f"Error type: {type(error).__name__}")
    print(f"Error message: {str(error)}")
    print(f"Error args: {error.args}")

The variable error holds the exception instance. You can inspect the error message, access the exception type, or retrieve the arguments passed when the exception was raised. This information helps with debugging and logging.

Using else clauses with python try except

The else clause executes only when the try block completes without raising any exception. This keeps success logic separate from error handling.

try:
    file = open("config.json", "r")
    data = json.load(file)
except FileNotFoundError:
    print("Config file missing, using defaults")
    data = default_config()
else:
    print("Config loaded successfully")
    validate_config(data)
finally:
    print("Configuration process complete")

The validation code in the else block runs only after successfully loading the file. Placing this code in the try block would be risky because any errors during validation would incorrectly trigger the FileNotFoundError handler. The else clause prevents this confusion.

Implementing finally blocks for cleanup operations

The finally block always executes, regardless of whether an exception occurred. This guarantees cleanup code runs even when errors happen.

connection = None
try:
    connection = database.connect()
    result = connection.execute(query)
    return result
except DatabaseError as error:
    log_error(error)
    raise
finally:
    if connection:
        connection.close()

Database connections must be closed properly to prevent resource leaks. The finally block ensures the connection closes whether the query succeeds, fails, or raises an exception. This pattern applies to any resource that requires cleanup, including file handles and network sockets.

Raising exceptions in python

You can trigger exceptions deliberately using the raise keyword. This helps enforce business logic and data validation rules.

def withdraw(account, amount):
    if amount <= 0:
        raise ValueError("Withdrawal amount must be positive")
    if amount > account.balance:
        raise ValueError("Insufficient funds")
    account.balance -= amount
    return account.balance

Raising exceptions communicates error conditions to calling code. The ValueError exceptions signal that something went wrong, and the error messages explain the specific problem. This approach is clearer than returning error codes or special values.

Re-raising exceptions after logging

Sometimes you want to log an error but still propagate it to higher-level code. Use a bare raise statement inside an except block.

try:
    response = requests.get(api_url, timeout=5)
    response.raise_for_status()
except requests.RequestException as error:
    logger.error(f"API request failed: {error}")
    raise

The except block logs the error for monitoring purposes. The bare raise statement then re-throws the same exception without modifying it. This preserves the original stack trace, making debugging easier.

Chaining exceptions with raise from

Exception chaining links a new exception to the original one. This creates a complete error history showing how one problem led to another.

try:
    raw_data = load_from_cache()
except CacheError as error:
    raise DataLoadError("Failed to load data") from error

The from keyword establishes a causal relationship between exceptions. Python’s traceback displays both the CacheError and the DataLoadError, showing the complete chain of events. This context helps identify root causes faster than seeing only the final exception.

You can suppress the automatic chaining by using from None.

try:
    result = parse_json(data)
except json.JSONDecodeError:
    raise ValueError("Invalid data format") from None

This shows only the ValueError in the traceback, hiding the JSONDecodeError. Use this sparingly, as it removes potentially valuable debugging information.

Handling file operations with python try except

File operations are prone to multiple failure modes. Combining exception handling with context managers creates robust file handling code.

def read_configuration(filename):
    try:
        with open(filename, 'r') as file:
            content = file.read()
            config = json.loads(content)
            return config
    except FileNotFoundError:
        print(f"Config file {filename} not found")
        return create_default_config()
    except json.JSONDecodeError as error:
        print(f"Invalid JSON in config file: {error}")
        return create_default_config()
    except PermissionError:
        print(f"No permission to read {filename}")
        raise

The with statement handles file closing automatically. The try except structure catches specific errors without hiding unexpected exceptions like PermissionError, which gets re-raised to alert administrators of access control issues.

Validating user input with python try except

Python try except blocks excel at handling unpredictable user input. Converting strings to numbers is a common scenario requiring validation.

def get_integer_input(prompt, min_value=None, max_value=None):
    while True:
        try:
            value = int(input(prompt))
            if min_value is not None and value < min_value:
                print(f"Value must be at least {min_value}")
                continue
            if max_value is not None and value > max_value:
                print(f"Value must be at most {max_value}")
                continue
            return value
        except ValueError:
            print("Please enter a valid integer")
        except KeyboardInterrupt:
            print("\nInput cancelled")
            raise

The loop continues until the user provides valid input. The ValueError exception handles non-numeric input, while the range checks enforce business rules. KeyboardInterrupt gets re-raised because user cancellation should propagate to the calling code.

Working with API requests using python try except

Network operations introduce latency and failure points. Proper exception handling makes API integrations more resilient.

import requests
from requests.exceptions import RequestException, Timeout, HTTPError

def fetch_user_data(user_id, max_retries=3):
    url = f"https://api.example.com/users/{user_id}"
    
    for attempt in range(max_retries):
        try:
            response = requests.get(url, timeout=10)
            response.raise_for_status()
            return response.json()
        except Timeout:
            print(f"Request timeout (attempt {attempt + 1}/{max_retries})")
            if attempt == max_retries - 1:
                raise
        except HTTPError as error:
            if error.response.status_code == 404:
                return None
            print(f"HTTP error: {error}")
            raise
        except RequestException as error:
            print(f"Request failed: {error}")
            raise

This function implements retry logic for timeout errors while immediately failing on HTTP errors. The 404 status returns None instead of raising an exception, treating missing users as a valid scenario. Other HTTP errors and connection problems propagate immediately.

Creating custom exceptions in python

Custom exceptions make your code more expressive and easier to handle. They communicate domain-specific problems clearly.

class InsufficientFundsError(Exception):
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(f"Cannot withdraw {amount}. Balance: {balance}")

class AccountLockedError(Exception):
    pass

def process_withdrawal(account, amount):
    if account.is_locked:
        raise AccountLockedError("Account is locked")
    if amount > account.balance:
        raise InsufficientFundsError(account.balance, amount)
    account.balance -= amount

Custom exceptions carry contextual data. InsufficientFundsError stores both the current balance and the requested amount. Calling code can catch this specific exception and access these attributes to provide detailed error messages to users.

Best practices for python try except blocks

Keep try blocks small and focused. Only wrap code that might raise exceptions, not entire functions.

# Better approach
def process_order(order_data):
    validate_order_structure(order_data)
    
    try:
        customer = Customer.objects.get(id=order_data['customer_id'])
    except Customer.DoesNotExist:
        raise ValueError("Customer not found")
    
    try:
        payment = process_payment(order_data['payment_info'])
    except PaymentError as error:
        log_payment_failure(error)
        raise
    
    create_order(customer, order_data, payment)

This approach makes error sources obvious. Each try block contains exactly the operation that might fail. Avoid catching Exception without re-raising it, as this swallows all errors including KeyboardInterrupt and SystemExit.

Never use bare except clauses except in very specific cases like logging all exceptions before re-raising them.

# Avoid this
try:
    risky_operation()
except:
    print("Something went wrong")

# Use this instead
try:
    risky_operation()
except Exception as error:
    logger.exception("Unexpected error in risky_operation")
    raise

The bare except catches system exceptions that should propagate. Catching Exception explicitly makes your intent clear while still allowing program termination signals through.

Debugging exceptions in python

Python’s traceback shows the complete call stack when an exception occurs. Reading tracebacks from bottom to top reveals the error progression.

def calculate_total(items):
    return sum(item['price'] * item['quantity'] for item in items)

def process_cart(cart_data):
    try:
        total = calculate_total(cart_data['items'])
        return {'total': total, 'status': 'success'}
    except KeyError as error:
        return {
            'total': 0,
            'status': 'error',
            'message': f'Missing required field: {error}',
            'cart_data': cart_data
        }
    except TypeError as error:
        return {
            'total': 0,
            'status': 'error',
            'message': f'Invalid data type: {error}',
            'cart_data': cart_data
        }

Including the original data in error responses helps reproduce and fix bugs. The error dictionary contains both the exception message and the input that caused the problem.

Python exception handling turns potential crashes into managed error states. The try except mechanism lets you anticipate problems, respond appropriately, and maintain program stability. Whether validating input, accessing resources, or communicating with external services, proper exception handling separates robust code from fragile implementations.

Pankaj Kumar
Pankaj Kumar

I have been working on Python programming for more than 12 years. At AskPython, I share my learning on Python with other fellow developers.

Articles: 235