Python Exceptions

Handle errors gracefully with try/except/finally, raise custom exceptions, chain exceptions, and understand Python's exception hierarchy.

Beginner 12 min read 10 examples

try / except

Use try/except to handle errors without crashing. Catch specific exceptions - never use a bare except:.

Python
# Basic try/except
try:
    x = int(input("Enter a number: "))
    result = 100 / x
    print(f"100 / {x} = {result}")
except ValueError:
    print("That is not a valid number")
except ZeroDivisionError:
    print("Cannot divide by zero")

# Catch multiple exceptions in one clause
try:
    data = int("not a number")
except (ValueError, TypeError) as e:
    print(f"Conversion error: {e}")

# Access exception with 'as e'
try:
    with open("missing.txt") as f:
        content = f.read()
except FileNotFoundError as e:
    print(f"File not found: {e.filename}")
    print(f"Error code: {e.errno}")
    content = ""

# Never use bare except - it swallows everything including Ctrl+C
# BAD:
# try:
#     ...
# except:       # catches KeyboardInterrupt, SystemExit too
#     pass

# GOOD: catch specific or at least Exception
try:
    risky_operation = 1 / 0
except Exception as e:
    print(f"Caught: {type(e).__name__}: {e}")

# Check exception type
try:
    result = 1 / 0
except Exception as e:
    if isinstance(e, ZeroDivisionError):
        print("Zero division!")
    raise   # re-raise the same exception

else and finally

Python
# else: runs only if NO exception was raised
try:
    result = int("42")
except ValueError:
    print("Bad input")
else:
    print(f"Success: {result}")     # runs because no exception

# finally: ALWAYS runs (cleanup code)
import os
f = None
try:
    f = open("data.txt", "w")
    f.write("test")
    raise RuntimeError("something failed")
except RuntimeError as e:
    print(f"Error: {e}")
finally:
    if f:
        f.close()
        print("File closed")    # runs even though exception occurred

# With 'with' statement this is cleaner:
try:
    with open("data.txt", "w") as f:
        f.write("test")
        raise RuntimeError("something failed")
except RuntimeError as e:
    print(f"Error: {e}")
    # file is automatically closed by 'with'

# Full try/except/else/finally structure
def divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("Division by zero!")
        return None
    else:
        print(f"Result: {result}")   # only if no exception
        return result
    finally:
        print("divide() called")     # always runs

print(divide(10, 2))    # "Result: 5.0", "divide() called", 5.0
print(divide(10, 0))    # "Division by zero!", "divide() called", None

# finally and return: finally ALWAYS runs
def risky():
    try:
        return "try"
    finally:
        return "finally"    # overrides "try" return!

print(risky())  # "finally"  (finally return takes precedence)

raise and re-raise

Python
# Raise a built-in exception
def get_age(age):
    if not isinstance(age, int):
        raise TypeError(f"age must be int, got {type(age).__name__}")
    if age < 0:
        raise ValueError(f"age cannot be negative: {age}")
    if age > 150:
        raise ValueError(f"age seems unrealistic: {age}")
    return age

try:
    get_age(-5)
except ValueError as e:
    print(e)    # age cannot be negative: -5

# Re-raise: run code, then propagate the original exception
def process_file(path):
    try:
        with open(path) as f:
            return f.read()
    except OSError:
        print(f"Failed to read {path}")
        raise   # re-raises the same exception with original traceback

# Raise from inside except (avoid losing original exception)
def connect(url):
    try:
        # simulate connection
        raise ConnectionRefusedError("port 5432")
    except ConnectionRefusedError as e:
        raise RuntimeError(f"Database unavailable: {url}") from e

# raise ... from None: suppress chained traceback
def parse_config(text):
    try:
        import json
        return json.loads(text)
    except json.JSONDecodeError as e:
        raise ValueError(f"Invalid config format: {e}") from None
        # caller doesn't need to see the JSONDecodeError details

Custom Exceptions

Python
# Simple custom exception
class AppError(Exception):
    """Base class for all application errors."""
    pass

class ValidationError(AppError):
    """Raised when input validation fails."""
    pass

class NotFoundError(AppError):
    """Raised when a resource is not found."""
    pass

# Custom exception with extra attributes
class HttpError(Exception):
    def __init__(self, status_code, message):
        super().__init__(message)   # sets args[0] and str(self)
        self.status_code = status_code
        self.message     = message

    def __str__(self):
        return f"HTTP {self.status_code}: {self.message}"

# Usage
try:
    raise HttpError(404, "Not Found")
except HttpError as e:
    print(e.status_code)    # 404
    print(e.message)        # Not Found
    print(str(e))           # HTTP 404: Not Found

# Exception hierarchy for a library
class DatabaseError(Exception):
    """Base class for database errors."""
    pass

class ConnectionError(DatabaseError):
    def __init__(self, host, port, original=None):
        super().__init__(f"Cannot connect to {host}:{port}")
        self.host     = host
        self.port     = port
        self.original = original

class QueryError(DatabaseError):
    def __init__(self, query, message):
        super().__init__(f"Query failed: {message}")
        self.query = query

# Callers can catch at any level
try:
    raise ConnectionError("localhost", 5432)
except ConnectionError:
    print("Connection issue - retry")
except DatabaseError:
    print("Some database error")
except Exception:
    print("Unexpected error")

Exception Chaining

Python
# Implicit chaining - Python auto-chains when raising inside except
try:
    try:
        int("not a number")
    except ValueError:
        open("missing.txt")     # raises FileNotFoundError
except FileNotFoundError as e:
    print(e.__context__)    # ValueError: invalid literal...
    # Traceback shows both exceptions

# Explicit chaining with 'from'
class ServiceError(Exception):
    pass

def call_api():
    try:
        # simulate failed HTTP call
        raise ConnectionRefusedError("Port 443")
    except ConnectionRefusedError as e:
        raise ServiceError("API unavailable") from e

try:
    call_api()
except ServiceError as e:
    print(e)                # API unavailable
    print(e.__cause__)      # ConnectionRefusedError - the original
    # Traceback: "The above exception was the direct cause of..."

# Suppress chaining with 'from None'
def load_config(path):
    try:
        import json
        with open(path) as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError) as e:
        raise ValueError(f"Cannot load config from {path}") from None
        # Original exception details hidden from caller

# Context manager for error suppression
from contextlib import suppress

with suppress(FileNotFoundError):
    import os
    os.remove("temp_file.txt")  # silently ignore if missing

Exception Hierarchy

Python
# Common built-in exceptions and when they occur

# ValueError - right type, wrong value
int("abc")          # ValueError: invalid literal
int("")             # ValueError
[1,2,3].index(99)   # ValueError: 99 is not in list

# TypeError - wrong type
"2" + 2             # TypeError: can only concatenate str (not "int")
len(42)             # TypeError: object of type 'int' has no len()

# KeyError - dict key not found
d = {"a": 1}
d["b"]              # KeyError: 'b'

# IndexError - sequence index out of range
lst = [1, 2, 3]
lst[5]              # IndexError: list index out of range

# AttributeError - object has no attribute
None.strip()        # AttributeError: 'NoneType' object has no attribute 'strip'

# NameError - name not defined
print(undefined_var)  # NameError: name 'undefined_var' is not defined

# ImportError / ModuleNotFoundError
import nonexistent   # ModuleNotFoundError: No module named 'nonexistent'

# FileNotFoundError (subclass of OSError)
open("missing.txt")  # FileNotFoundError

# OverflowError, ZeroDivisionError, RecursionError
1 / 0               # ZeroDivisionError

# StopIteration - iterator exhausted
it = iter([1])
next(it)
next(it)            # StopIteration

# Best practice: catch the most specific exception possible
def safe_divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return float("inf")
Catch the most specific exception you can handle

Catch only the exceptions you know how to handle. A broad except Exception hides bugs. If you genuinely want to catch everything (for logging), still re-raise: except Exception: log_error(); raise. Never silently swallow exceptions with except Exception: pass unless you are absolutely certain silence is correct.

Frequently Asked Questions

BaseException is the root of all exceptions including SystemExit, KeyboardInterrupt, and GeneratorExit. Exception is a subclass of BaseException that covers all normal program errors. Use except Exception: to catch normal errors without catching system signals like Ctrl+C (KeyboardInterrupt). Never use bare except: - it catches everything including SystemExit.

Create custom exceptions when: (1) you want to raise errors specific to your library/module that callers can catch independently of built-in errors, (2) you need to attach extra data to exceptions (like an error code or HTTP status), (3) you want to create an exception hierarchy for different error categories. Keep custom exceptions light - subclass the most specific built-in that fits, and add only the attributes you need.

raise NewException("msg") from original_exception creates an explicit exception chain - the traceback shows both the original cause and the new exception. Use it when translating low-level exceptions into higher-level ones: except OSError as e: raise DatabaseError("Cannot connect") from e. This preserves the original cause for debugging. Using raise ... from None suppresses the chain when the cause is irrelevant to the caller.

Always use except Exception: or a more specific exception class. Never use bare except: because it catches SystemExit (from sys.exit()), KeyboardInterrupt (Ctrl+C), and GeneratorExit - things you almost never want to catch. Bare except makes programs difficult to terminate and hides serious errors.