DocsHub
Error Handling

Exceptions

Learn how to handle errors in Python using try, except, else, finally, and raise.

Exceptions

Things go wrong in programs. A file does not exist. A user types a letter where you expected a number. A network request fails. These are called exceptions — events that interrupt the normal flow of your program.

Without handling them, your program crashes and shows an ugly error message. With proper exception handling, you can catch the problem, respond to it gracefully, and keep your program running.

# without handling — program crashes
number = int(input("Enter a number: "))   # user types "hello"
# ValueError: invalid literal for int() with base 10: 'hello'

# with handling — program responds gracefully
try:
    number = int(input("Enter a number: "))
    print(f"You entered: {number}")
except ValueError:
    print("That is not a valid number. Please try again.")

Common built-in exceptions

Before handling exceptions, know what you are dealing with:

ExceptionWhen it happens
ValueErrorRight type, wrong value — int("hello")
TypeErrorWrong type — "text" + 5
IndexErrorList index out of range — items[99]
KeyErrorDictionary key not found — d["missing"]
AttributeErrorObject has no such attribute — "hi".push()
FileNotFoundErrorFile does not exist — open("nope.txt")
ZeroDivisionErrorDividing by zero — 10 / 0
NameErrorVariable not defined — print(x) before x = ...
ImportErrorModule not found — import nope
StopIterationIterator has no more items
RecursionErrorToo many nested function calls
PermissionErrorNo permission to access a file
TimeoutErrorOperation took too long
MemoryErrorNot enough memory

try and except — the basics

Wrap risky code in a try block. If an exception happens, Python jumps to the except block:

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

Output:

Cannot divide by zero.

Without the try/except, this would crash. With it, the error is caught and handled.


How try/except/else/finally flows

exception raised no exception Start try block runs except block runs else block runs finally block runs Continue
  • try — the code you want to run
  • except — runs only if an exception was raised
  • else — runs only if NO exception was raised
  • finally — always runs no matter what

Catching specific exceptions

Always catch the most specific exception you can. Do not catch everything blindly:

try:
    age = int(input("Enter your age: "))
    result = 100 / age
    print(f"100 divided by your age is {result:.2f}")
except ValueError:
    print("Please enter a valid number.")
except ZeroDivisionError:
    print("Age cannot be zero.")

Python checks each except from top to bottom and runs the first one that matches.


Catching multiple exceptions in one line

try:
    value = int(input("Enter a number: "))
    result = 10 / value
except (ValueError, ZeroDivisionError):
    print("Invalid input — enter a non-zero number.")

Getting the exception details

Use as to capture the exception object and read its message:

try:
    number = int("hello")
except ValueError as e:
    print(f"Error: {e}")
    # Error: invalid literal for int() with base 10: 'hello'
try:
    with open("missing.txt", "r") as f:
        content = f.read()
except FileNotFoundError as e:
    print(f"File error: {e}")
    # File error: [Errno 2] No such file or directory: 'missing.txt'

else — runs when no exception occurred

The else block runs only if the try block completed without any exception. It is the right place for code that should only run on success:

try:
    age = int(input("Enter your age: "))
except ValueError:
    print("That is not a valid number.")
else:
    # only runs if int() succeeded
    if age >= 18:
        print("You are an adult.")
    else:
        print("You are a minor.")

Why use else instead of just putting the code at the end of the try block? Because code in else is protected — if it raises an exception, it will NOT be caught by the except above it. This prevents accidentally hiding bugs.


finally — always runs

finally runs no matter what — whether an exception happened or not, whether it was caught or not. Use it for cleanup — closing files, releasing resources, closing database connections:

try:
    file = open("data.txt", "r")
    content = file.read()
    result = int(content)
except FileNotFoundError:
    print("File not found.")
except ValueError:
    print("File does not contain a valid number.")
finally:
    print("Cleaning up...")
    # this always runs

A common real pattern — ensure a resource is released:

connection = None

try:
    connection = connect_to_database()
    data = connection.fetch("SELECT * FROM users")
except DatabaseError as e:
    print(f"Database error: {e}")
finally:
    if connection:
        connection.close()   # always close the connection

In modern Python you rarely need finally for file handling because with handles closing automatically. But for databases, network connections, and other resources, finally is still essential.


Catching all exceptions

You can catch any exception with a bare except Exception:

try:
    risky_operation()
except Exception as e:
    print(f"Something went wrong: {e}")

Be careful with catching all exceptions. It can hide bugs — you might swallow an error you did not expect and have no idea why your program is not working correctly. Always prefer catching specific exceptions. Only catch Exception at the top level of your program where you want to prevent crashes and log the error.

Never use a bare except: without Exception — it catches even system exits and keyboard interrupts:

# bad — catches Ctrl+C and sys.exit() too
try:
    risky()
except:
    pass

# better — only catches real exceptions
try:
    risky()
except Exception as e:
    print(f"Error: {e}")

raise — throwing exceptions manually

You can raise exceptions yourself when something goes wrong in your own logic:

def set_age(age):
    if not isinstance(age, int):
        raise TypeError(f"Age must be an integer, got {type(age).__name__}")
    if age < 0 or age > 150:
        raise ValueError(f"Age must be between 0 and 150, got {age}")
    return age

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

Output:

Invalid age: Age must be between 0 and 150, got -5

raise from — exception chaining

When you catch one exception and raise another, use raise from to keep the original context:

def load_config(path):
    try:
        with open(path, "r") as f:
            return json.load(f)
    except FileNotFoundError as e:
        raise RuntimeError(f"Config file missing: {path}") from e

The original FileNotFoundError is preserved as context — when you see the traceback, you see both exceptions. This makes debugging much easier.

If you deliberately want to hide the original exception:

raise RuntimeError("Config failed") from None

re-raising an exception

Catch an exception, do something with it, then re-raise it to let it bubble up:

def process_file(path):
    try:
        with open(path, "r") as f:
            return f.read()
    except FileNotFoundError as e:
        log_error(f"File not found: {path}")   # log it
        raise   # re-raise the original exception

Plain raise with no argument re-raises the current exception exactly as it was.


Nested try/except

You can nest try blocks inside each other:

def get_user_score():
    try:
        raw = input("Enter your score (0-100): ")
        try:
            score = int(raw)
        except ValueError:
            print(f"'{raw}' is not a number. Using 0.")
            score = 0

        if score < 0 or score > 100:
            raise ValueError(f"Score must be 0-100, got {score}")

        return score

    except ValueError as e:
        print(f"Invalid score: {e}")
        return None

A real example — a robust file reader

import json
from pathlib import Path

def load_json_file(filepath):
    """
    Load a JSON file safely.
    Returns the data or None if anything goes wrong.
    """
    path = Path(filepath)

    try:
        if not path.exists():
            raise FileNotFoundError(f"File not found: {filepath}")

        if path.suffix != ".json":
            raise ValueError(f"Expected a .json file, got {path.suffix}")

        content = path.read_text(encoding="utf-8")
        data = json.loads(content)

    except FileNotFoundError as e:
        print(f"File error: {e}")
        return None
    except ValueError as e:
        print(f"Format error: {e}")
        return None
    except json.JSONDecodeError as e:
        print(f"JSON parse error in {filepath}: {e}")
        return None
    except PermissionError:
        print(f"No permission to read {filepath}")
        return None
    else:
        print(f"Successfully loaded {filepath}")
        return data
    finally:
        print(f"Finished processing {filepath}")


data = load_json_file("config.json")

if data:
    print(data)

Exception hierarchy

Python's built-in exceptions follow an inheritance tree. When you catch a parent class, you catch all its children too:

BaseException
├── SystemExit
├── KeyboardInterrupt
└── Exception
    ├── ValueError
    ├── TypeError
    ├── OSError
    │   ├── FileNotFoundError
    │   ├── PermissionError
    │   └── TimeoutError
    ├── LookupError
    │   ├── IndexError
    │   └── KeyError
    └── ... (many more)

So catching OSError catches FileNotFoundError, PermissionError, and TimeoutError all at once:

try:
    with open("data.txt", "r") as f:
        content = f.read()
except OSError as e:
    print(f"File system error: {e}")

Summary

KeywordPurpose
tryWrap risky code
except ErrorTypeHandle a specific exception
except (A, B)Handle multiple exceptions
except ErrorType as eCapture exception details
elseRun if no exception occurred
finallyAlways runs — cleanup code
raise ErrorType("msg")Raise an exception manually
raiseRe-raise the current exception
raise X from YChain exceptions with context

On this page