New in 2026: Master Python for AI, Data Science

Python

Python Exception Handling

Python exception handling guide — try/except, custom exceptions, tracebacks, and best practices explained with examples.

Your Python program crashes. A file is missing, a list index is out of bounds, a network request times out. Every serious program encounters these situations. The question isn’t whether errors happen — it’s whether your code handles them gracefully. Python’s exception handling is one of the cleanest in any programming language. Here’s everything you need to know.

In this guide you will learn:

  • The difference between syntax errors and runtime exceptions
  • How to use try/except to catch and handle exceptions
  • The else and finally clauses and when to use them
  • How to raise your own custom exceptions
  • Real output from running each code example

Syntax Errors vs Exceptions

Python has two categories of errors. Syntax errors (parsing errors) are problems with the program’s structure — Python can’t even parse the code. Exceptions are runtime errors that occur during execution — the code is valid Python, but something went wrong when it ran.

x = [1, 2, 3]
print(x[10])  # IndexError: list index out of range

The try/except Block

The basic pattern: try to run some code, and if a specific exception occurs, handle it.

# Catch a ZeroDivisionError
try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"Caught: {type(e).__name__}: {e}")

Output

Caught: ZeroDivisionError: division by zero
# Catch a KeyError from a dictionary
try:
    d = {'a': 1}
    print(d['nonexistent'])
except KeyError as e:
    print(f"Caught: {type(e).__name__}: {e}")

Output

Caught: KeyError: 'nonexistent'
# Catch a FileNotFoundError
try:
    with open('/nonexistent/file.txt') as f:
        content = f.read()
except FileNotFoundError as e:
    print(f"Caught: {type(e).__name__}: {e}")

Output

Caught: FileNotFoundError: [Errno 2] No such file or directory: '/nonexistent/file.txt'

Catching Multiple Exceptions

def get_item(data, key, index):
    try:
        value = data[key]
        return value[index]
    except (KeyError, IndexError, TypeError) as e:
        print(f"Error accessing data: {type(e).__name__}: {e}")
        return None

# Test with different errors
print(get_item({'a': [1, 2, 3]}, 'a', 5))   # IndexError
print(get_item({}, 'b', 0))                    # KeyError
print(get_item('not a dict', 'key', 0))       # TypeError

Output

Error accessing data: IndexError: list index out of range
Error accessing data: KeyError: 'b'
Error accessing data: TypeError: string indices must be integers

The else Clause

The else block runs only if no exception was raised in the try block. The key difference from simply placing code after the try/except is that else runs only on success — if an exception is caught and handled, the else block is skipped entirely. This keeps success logic cleanly separated from error handling.

def divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("Cannot divide by zero")
    else:
        print(f"Division succeeded: {result}")
        return result
    finally:
        print("Cleanup done")

print(divide(10, 2))

Output

Division succeeded: 5.0
Cleanup done
5.0

The finally Clause

The finally block always executes — ideal for cleanup:

def read_config(filename):
    f = None
    try:
        f = open(filename, 'r')
        return f.read()
    except FileNotFoundError:
        print(f"Config file '{filename}' not found")
        return {}
    finally:
        if f:
            f.close()
            print("File handle closed")

result = read_config('/etc/app/config.json')
print(f"Result: {result}")

Output

Config file '/etc/app/config.json' not found
File handle closed
Result: {}

try/except/else/finally — Full Pattern

import json

def parse_json_safe(json_string):
    try:
        data = json.loads(json_string)
    except json.JSONDecodeError as e:
        print(f"JSON decode error: {e}")
        return None
    else:
        print("JSON parsed successfully")
        return data
    finally:
        print("parse_json_safe() finished")

print("=== Valid JSON ===")
parse_json_safe('{"name": "Alice", "age": 30}')

print("n=== Invalid JSON ===")
parse_json_safe('not valid json at all')

Output

=== Valid JSON ===
JSON parsed successfully
parse_json_safe() finished
=== Invalid JSON ===
JSON decode error: Expecting value: line 1 column 1 (char 0)
parse_json_safe() finished

Raising Custom Exceptions

For domain-specific errors, define custom exception classes that carry meaningful context. This makes error handling more expressive and helps debugging in larger applications.

class InvalidAgeError(Exception):
    def __init__(self, age, message=None):
        self.age = age
        self.message = message or f"Age cannot be negative: {age}"
        super().__init__(self.message)

def set_age(age):
    if age  150:
        raise InvalidAgeError(age, f"Age {age} is unreasonably high")
    return age

try:
    set_age(-5)
except InvalidAgeError as e:
    print(f"Caught custom exception: {e}")
    print(f"Invalid age value: {e.age}")

try:
    set_age(200)
except InvalidAgeError as e:
    print(f"Caught custom exception: {e}")
    print(f"Invalid age value: {e.age}")

Output

Caught custom exception: Age -5 is negative
Invalid age value: -5
Caught custom exception: Age 200 is unreasonably high
Invalid age value: 200

Common Built-in Exceptions

ExceptionWhen It’s Raised
ZeroDivisionErrorDivision or modulo by zero
IndexErrorSequence index out of range
KeyErrorDictionary key not found
FileNotFoundErrorFile does not exist
TypeErrorOperation on incompatible types
ValueErrorArgument has wrong value (correct type, wrong value)
AttributeErrorObject has no attribute
NameErrorVariable or function name not defined
ImportErrorModule import failed

Best Practices

  • Be specific — catch the exact exception type, not a bare except:. Bare except catches everything including KeyboardInterrupt and SystemExit, which masks bugs.
  • Keep try blocks small — only wrap the code that might raise. A large try block makes it hard to know which line caused the exception.
  • Don’t suppress errors silently — at minimum, log the error or re-raise it. Silently swallowing exceptions makes debugging painful.
  • Use finally for cleanup — close files, release locks, close database connections. The finally block runs even if an exception is raised.
  • Use else for success-only code — the else block runs only when no exception occurred, keeping success logic separate from error handling.
  • Define custom exceptions — for domain-specific errors, create custom exception classes that carry meaningful context.

Summary

Python’s exception handling is expressive and clean. The try/except/else/finally pattern covers every scenario. Custom exceptions let you define domain-specific errors that carry meaningful context. Python’s official exception documentation lists all built-in types. Mastering exception handling is the difference between scripts that crash and programs that fail gracefully.

If you’re new to Python, start with Introduction to Python Programming to build a solid foundation. For deeper context on writing robust Python code, see Python Debugging Techniques to learn how to trace and fix exceptions in real programs.

Related posts
Python

Pydantic Agent Basics: A Complete 2026 Tutorial

ProgrammingPython

Production-Ready MCP Servers — Security, Testing & Deployment

ProgrammingPython

Build Your First MCP Server with Python SDK — Fundamentals

ProgrammingPython

Connect FastAPI to MCP — Two Integration Patterns

Leave a Reply