Last updated: 2026-05-22

Python error handling

Errors are inevitable in software. Files don't exist, network connections fail, users enter unexpected input, and APIs return unexpected responses. Error handling is how a program responds to these situations gracefully — giving useful feedback, recovering where possible, and failing clearly when not. Python's error handling system is clean, explicit, and built around exceptions.


Errors vs. Exceptions

Python distinguishes between two types of problems:

Syntax errors occur when Python cannot parse your code — the code is not valid Python. These are caught before the program runs:

if True
    print("missing colon")    # SyntaxError: expected ':'

Exceptions occur during execution — the code is valid Python, but something goes wrong at runtime:

print(10 / 0)           # ZeroDivisionError
print(int("hello"))     # ValueError
print(name)             # NameError: name 'name' is not defined

Error handling deals with exceptions — syntax errors must be fixed in the code before the program can run.


The try / except Block

The fundamental error handling structure in Python:

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

Python attempts to execute the try block. If an exception occurs, execution jumps immediately to the matching except block. If no exception occurs, the except block is skipped entirely.

Catching multiple exceptions

try:
    value = int(input("Enter a number: "))
    result = 100 / value
    print(result)
except ValueError:
    print("That's not a valid number.")
except ZeroDivisionError:
    print("Cannot divide by zero.")

Python checks each except clause in order and executes the first one that matches the raised exception.

Catching multiple exceptions in one clause

try:
    value = int(input("Enter a number: "))
except (ValueError, TypeError):
    print("Invalid input.")

Catching any exception

try:
    risky_operation()
except Exception as e:
    print(f"An error occurred: {e}")

Exception is the base class for all non-system-exiting exceptions. Catching it handles everything but SystemExit, KeyboardInterrupt, and GeneratorExit. Avoid catching bare except: without specifying a type — it catches everything including KeyboardInterrupt (Ctrl+C), making the program impossible to interrupt.

Accessing the exception object

try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"Error: {e}")          # Error: division by zero
    print(type(e).__name__)       # ZeroDivisionError

The as e syntax binds the exception instance to a variable so you can inspect its message and type.


else and finally

The full try block structure has four clauses:

try:
    # Code that might raise an exception
except SomeException:
    # Runs if that exception occurs
else:
    # Runs only if NO exception occurred
finally:
    # Always runs — exception or not

else

The else block runs only when the try block completes without raising an exception. It is the place for code that should only execute if everything went smoothly:

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

Using else makes it clear that content = file.read() should not run if the file wasn't opened successfully.

finally

The finally block always runs — whether an exception occurred or not, whether it was caught or not. It is used for cleanup operations that must happen regardless:

file = None
try:
    file = open("data.txt", "r")
    content = file.read()
except FileNotFoundError:
    print("File not found.")
finally:
    if file:
        file.close()    # Always close the file, even if an error occurred
    print("Done.")

In practice, with statements handle cleanup more cleanly than finally for resources like files. finally is most useful for network connections, database sessions, or lock releases — resources without a context manager.


Common Built-in Exceptions

Python has a rich hierarchy of built-in exceptions. The most frequently encountered:

ExceptionWhen it occurs
ValueErrorFunction receives the right type but an invalid value — int("hello")
TypeErrorOperation applied to the wrong type — "5" + 3
NameErrorVariable or function name not defined
IndexErrorList index out of range — [1, 2, 3][5]
KeyErrorDictionary key does not exist — d["missing"]
AttributeErrorAttribute or method does not exist on an object
FileNotFoundErrorFile or directory does not exist
PermissionErrorInsufficient permissions
ZeroDivisionErrorDivision or modulo by zero
OverflowErrorResult too large to represent
ImportErrorModule cannot be found or imported
ModuleNotFoundErrorSubclass of ImportError — module does not exist
StopIterationIterator has no more items
RuntimeErrorGeneric error that doesn't fit other categories
NotImplementedErrorAbstract method has not been implemented
OSErrorOperating system error — base class for file and I/O errors

The exception hierarchy

BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
    ├── ValueError
    ├── TypeError
    ├── NameError
    ├── IndexError
    ├── KeyError
    ├── AttributeError
    ├── OSError
    │   ├── FileNotFoundError
    │   ├── PermissionError
    │   └── ...
    └── ...

Catching a parent class catches all its subclasses. Catching OSError will also catch FileNotFoundError and PermissionError.


Raising Exceptions — raise

You can raise exceptions deliberately in your own code to signal that something has gone wrong:

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

set_age("twenty")    # TypeError: Age must be an integer, got str
set_age(200)         # ValueError: Age must be between 0 and 150, got 200

Raising exceptions from your own code makes validation explicit and gives callers clear, descriptive error messages.

Re-raising an exception

Sometimes you want to catch an exception, do something (like log it), and then let it propagate:

try:
    risky_operation()
except ValueError as e:
    log_error(e)    # Log it
    raise           # Re-raise the same exception

Using bare raise (without arguments) re-raises the currently active exception, preserving the original traceback.


Custom Exceptions

For application-specific errors, define your own exception classes by inheriting from Exception or an appropriate subclass:

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

class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    def withdraw(self, amount):
        if amount > self.balance:
            raise InsufficientFundsError(amount, self.balance)
        self.balance -= amount
        return self.balance

account = BankAccount(100)

try:
    account.withdraw(150)
except InsufficientFundsError as e:
    print(e)               # Cannot withdraw 150. Balance is 100.
    print(e.amount)        # 150
    print(e.balance)       # 100

Custom exceptions make your error handling more expressive — callers can catch your specific exception type rather than a generic one, and the exception object can carry additional information.


Exception Chaining

When handling one exception causes another, Python can link them:

try:
    value = int("invalid")
except ValueError as e:
    raise RuntimeError("Failed to process input") from e

The from e syntax creates an explicit exception chain. When this RuntimeError is displayed, Python shows both the original ValueError and the new RuntimeError, making the full chain of events visible in the traceback.


Context Managers and __exit__

The with statement relies on the context manager protocol. When an exception occurs inside a with block, the context manager's __exit__ method is called with the exception information — allowing it to suppress or handle the exception:

class ManagedFile:
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        self.file = open(self.name, "r")
        return self.file

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.file.close()
        return False    # False means don't suppress the exception

with ManagedFile("data.txt") as f:
    content = f.read()

Returning True from __exit__ suppresses the exception. Returning False (or None) allows it to propagate. This is covered in more depth in the Python-Specific Features topic.


Best Practices

Be specific. Catch the most specific exception you can. Catching Exception everywhere hides bugs and makes debugging harder.

# Too broad — catches everything, hides problems
except Exception:
    pass

# Specific — clear intent
except FileNotFoundError:
    print("Config file missing — using defaults.")

Never silently suppress exceptions. A bare except: pass that swallows errors is one of the most dangerous patterns in Python:

# ❌ Never do this — errors disappear silently
try:
    do_something()
except:
    pass

Keep try blocks small. Only wrap the specific statement that might fail — not large blocks of code. This makes it clear exactly what you are guarding against:

# ❌ Too much in the try block
try:
    data = fetch_data()
    processed = process(data)
    save(processed)
except Exception:
    pass

# ✅ Specific
try:
    data = fetch_data()
except NetworkError as e:
    log(e)
    data = get_cached_data()

processed = process(data)
save(processed)

Use finally for cleanup, with when available. The with statement is cleaner for resources that support it. Reserve finally for cases where no context manager exists.

Provide useful error messages. Whether raising built-in or custom exceptions, the message should tell the caller what went wrong and ideally what the valid values are.


Summary

StatementPurpose
try: ... except E:Catch a specific exception
except (E1, E2):Catch multiple exception types
except E as e:Access the exception object
except Exception:Catch all non-system exceptions
else:Runs only if no exception occurred
finally:Always runs — used for cleanup
raise ExceptionType("msg")Raise an exception deliberately
raiseRe-raise the current exception
raise E from originalChain exceptions
class MyError(Exception):Define a custom exception