Handling Errors and Exceptions

· 6 min read · Updated March 7, 2026 · beginner
errors exceptions try-except error-handling beginner

Errors are a normal part of programming. A file might not exist, a network request might fail, or a user might enter invalid input. Rather than letting your program crash, you handle these situations gracefully with exception handling.

Python’s exception handling mechanism lets you catch errors and respond to them without stopping your program.

Understanding Exceptions

When Python encounters an error, it raises an exception. If that exception is not caught, the program terminates and displays an error message.

# This will crash because the file doesn't exist
with open("nonexistent.txt", "r") as file:
    content = file.read()

Running this produces a FileNotFoundError. The traceback shows exactly where the error occurred.

Catching Exceptions with try and except

The try block contains code that might raise an exception. The except block handles it:

try:
    with open("nonexistent.txt", "r") as file:
        content = file.read()
except FileNotFoundError:
    print("The file doesn't exist.")

Now the program continues instead of crashing. The user sees a friendly message instead of a traceback.

Handling Multiple Exception Types

A single try block can raise different types of exceptions. Handle each one separately:

try:
    with open("data.txt", "r") as file:
        number = int(file.read().strip())
        result = 100 / number
except FileNotFoundError:
    print("File not found.")
except ValueError:
    print("The file doesn't contain a valid number.")
except ZeroDivisionError:
    print("Cannot divide by zero.")

Python evaluates the except blocks in order and executes the first matching one.

Catching Multiple Exceptions in One Block

If multiple exceptions need the same handling, group them together:

try:
    result = int(user_input) / 0
except (ValueError, ZeroDivisionError):
    print("Invalid input or division by zero.")

Accessing the Exception Object

Sometimes you need details about the exception. The as keyword gives you access to it:

try:
    with open("config.txt", "r") as file:
        data = json.load(file)
except FileNotFoundError as e:
    print(f"File missing: {e.filename}")
except json.JSONDecodeError as e:
    print(f"Invalid JSON: {e.msg} at position {e.pos}")

The exception object contains useful attributes like filename, msg, and pos in this case.

The else Clause

The else block runs only if no exception was raised in the try block. Use it for code that should execute on success:

try:
    with open("data.txt", "r") as file:
        content = file.read()
except FileNotFoundError:
    print("File not found.")
else:
    print(f"Successfully read {len(content)} characters.")

This separates the “happy path” from error handling, making the code cleaner.

The finally Clause

The finally block runs regardless of whether an exception occurred. Use it for cleanup code that must execute:

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

A better approach uses the with statement, which handles closing automatically:

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

The with statement is a context manager that guarantees cleanup. Use it for file operations and other resources.

Raising Exceptions

You can raise exceptions yourself using the raise keyword. This is useful for validating input or signaling errors:

def divide(a, b):
    if b == 0:
        raise ValueError("Divisor cannot be zero.")
    return a / b

try:
    result = divide(10, 0)
except ValueError as e:
    print(f"Error: {e}")

Re-raising Exceptions

Inside an except block, you might want to log the error but still let it propagate:

try:
    dangerous_operation()
except Exception as e:
    print(f"Something went wrong: {e}")
    raise  # Re-raises the same exception

The bare raise statement re-raises the caught exception.

Raising a Different Exception

Sometimes you want to catch an exception and raise a different one:

try:
    with open("config.txt", "r") as file:
        config = json.load(file)
except FileNotFoundError:
    raise RuntimeError("Configuration file is missing.") from None

The from None suppresses the original exception chain. Use from original_exception to chain them.

Custom Exceptions

When your code has specific error conditions, create custom exception classes:

class InvalidAgeError(Exception):
    """Raised when age is not a positive integer."""
    pass

def set_age(age):
    if not isinstance(age, int):
        raise InvalidAgeError("Age must be an integer.")
    if age < 0:
        raise InvalidAgeError("Age cannot be negative.")
    return age

try:
    set_age(-5)
except InvalidAgeError as e:
    print(f"Invalid age: {e}")

Naming your exception with “Error” at the end follows Python conventions. Inherit from Exception directly for simple cases.

Custom Exceptions with Additional Data

Custom exceptions can carry extra information:

class ValidationError(Exception):
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")

try:
    raise ValidationError("email", "Invalid email format")
except ValidationError as e:
    print(f"Field: {e.field}, Error: {e.message}")

Exception Hierarchy

Python has a built-in exception hierarchy. Knowing it helps you catch the right exceptions:

├── SystemExit
├── KeyboardInterrupt
└── Exception
    ├── StopIteration
    ├── ArithmeticError
    │   ├── FloatingPointError
    │   ├── OverflowError
    │   └── ZeroDivisionError
    ├── LookupError
    │   ├── IndexError
    │   └── KeyError
    ├── OSError (IOError)
    │   ├── FileNotFoundError
    │   └── PermissionError
    ├── ValueError
    ├── TypeError
    └── ...

Catching a parent exception also catches its children. Be specific when you can:

# This catches ALL exceptions, including KeyboardInterrupt and SystemExit
# Don't do this unless you really mean it
try:
    risky_operation()
except Exception:
    print("Something went wrong")

Best Practices

Be Specific in except Blocks

Catch specific exceptions rather than using a bare except:

# Good
try:
    data = json.loads(user_input)
except json.JSONDecodeError:
    print("Invalid JSON")

# Avoid
try:
    data = json.loads(user_input)
except:
    print("Invalid JSON")

The bare except also catches KeyboardInterrupt and SystemExit, which usually shouldn’t be caught.

Use Exceptions for Exceptional Cases

Don’t use exceptions for flow control. If you can check a condition first, do so:

# Better - check first
if key in my_dict:
    value = my_dict[key]
else:
    value = default

# Works but less efficient
try:
    value = my_dict[key]
except KeyError:
    value = default

Exceptions are slow in Python because they involve creating objects and building tracebacks.

Clean Up with finally or Context Managers

Always release resources:

# Using finally
connection = None
try:
    connection = connect_to_server()
    data = connection.fetch()
finally:
    if connection:
        connection.close()

# Using context manager (preferred)
with connect_to_server() as connection:
    data = connection.fetch()

Example: A Robust Configuration Loader

Putting it all together, here’s a function that loads configuration with proper error handling:

import json
from pathlib import Path

class ConfigError(Exception):
    """Base exception for configuration errors."""
    pass

class ConfigNotFoundError(ConfigError):
    pass

class ConfigValidationError(ConfigError):
    pass

def load_config(path):
    """Load configuration from a JSON file."""
    # Check if file exists
    if not Path(path).exists():
        raise ConfigNotFoundError(f"Config file not found: {path}")
    
    try:
        with open(path, "r") as f:
            config = json.load(f)
    except json.JSONDecodeError as e:
        raise ConfigValidationError(f"Invalid JSON: {e}")
    
    # Validate required fields
    required = ["name", "version"]
    for field in required:
        if field not in config:
            raise ConfigValidationError(f"Missing required field: {field}")
    
    return config

# Usage
try:
    config = load_config("app_config.json")
    print(f"Loaded config: {config['name']} v{config['version']}")
except ConfigNotFoundError as e:
    print(f"Setup needed: {e}")
except ConfigValidationError as e:
    print(f"Config error: {e}")
except ConfigError as e:
    print(f"Unexpected config error: {e}")

This demonstrates custom exceptions, proper error hierarchy, validation, and clean error messages.

Next Steps

You now know how to handle errors gracefully in Python. These skills help you write programs that don’t crash unexpectedly and provide useful feedback to users.

To continue learning, explore these topics:

  • The traceback module for detailed error information
  • The logging module for recording errors in production
  • Context managers and the contextlib module
  • Testing exception handling with pytest

The next tutorial covers list, dict, and set comprehensions, which are powerful ways to create and transform collections in Python.