> ## Documentation Index
> Fetch the complete documentation index at: https://fastapi2day.codewithsiva.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Python Exception Handling: try, except & Custom Errors

> Handle runtime errors gracefully with try-except, raise custom exceptions, and use else and finally for clean resource management.

Programs encounter unexpected situations at runtime — a file may not exist, user input may be invalid, or a network request may fail. Without exception handling, any of these issues would crash your program. Python's `try`/`except` mechanism lets you gracefully detect and respond to errors, keep your application running, and communicate failures clearly. This page covers the full exception-handling toolkit: basic `try`/`except`, multiple error types, `else`/`finally`, raising exceptions, and writing your own custom exception classes.

## try, except, else, and finally

### Basic try-except

Wrap potentially failing code in a `try` block, and handle specific error types in `except` blocks:

```python theme={null}
try:
    number = int("not-a-number")
except ValueError:
    print("Please enter a valid integer")
```

### Catching Multiple Error Types

You can have several `except` clauses, each targeting a different exception:

```python theme={null}
try:
    with open("number.txt", "r") as f:
        text = f.read()
    number = int(text)
    result = 100 / number
except FileNotFoundError:
    print("Could not find the file")
except ValueError:
    print("File does not contain a valid number")
except ZeroDivisionError:
    print("Cannot divide by zero")
```

### else and finally

* **`else`**: Runs only if **no** exception was raised inside the `try` block.
* **`finally`**: Always runs, whether or not an exception occurred — ideal for clean-up tasks.

```python theme={null}
try:
    file = open("data.txt", "r")
    data = file.read()
except FileNotFoundError:
    print("File not found")
else:
    print("File loaded successfully")
finally:
    if "file" in locals() and not file.closed:
        file.close()
    print("Cleanup complete")
```

<Note>
  Use `finally` for releasing resources — closing files, database connections, network sockets — so they are always cleaned up even when exceptions occur.
</Note>

## Raising Exceptions

Use the `raise` keyword to manually trigger an exception when a business rule is violated:

```python theme={null}
def check_age(age: int):
    if age < 0:
        raise ValueError("Age cannot be negative!")
    if age < 18:
        raise ValueError("Must be at least 18 years old")
```

Callers can then catch the exception:

```python theme={null}
try:
    check_age(-5)
except ValueError as e:
    print(f"Validation error: {e}")
# Validation error: Age cannot be negative!
```

## Custom Exception Classes

For larger applications, Python's built-in exceptions may not carry enough domain-specific information. Inherit from `Exception` (or a more specific built-in) to create your own:

```python theme={null}
class InsufficientBalanceError(Exception):
    """Raised when a withdrawal amount exceeds the account balance."""
    def __init__(self, balance: float, amount: float):
        self.balance = balance
        self.amount = amount
        super().__init__(
            f"Attempted to withdraw ${amount} but only have ${balance}"
        )
```

<Steps>
  <Step title="Define the custom exception">
    Inherit from `Exception`. By convention, use an `Error` suffix.
  </Step>

  <Step title="Raise it in business logic">
    ```python theme={null}
    class BankAccount:
        def __init__(self, balance: float):
            self.balance = balance

        def withdraw(self, amount: float):
            if amount > self.balance:
                raise InsufficientBalanceError(self.balance, amount)
            self.balance -= amount
            print(f"Successfully withdrew ${amount}")
    ```
  </Step>

  <Step title="Catch and handle it">
    ```python theme={null}
    account = BankAccount(100)
    try:
        account.withdraw(150)
    except InsufficientBalanceError as error:
        print(f"Transaction Failed: {error}")
        print(f"Shortage: ${error.amount - error.balance}")
    ```
  </Step>
</Steps>

## Common Built-in Exceptions

| Exception           | Triggered by                          |
| ------------------- | ------------------------------------- |
| `ValueError`        | Wrong value type (e.g., `int("abc")`) |
| `TypeError`         | Wrong argument type                   |
| `KeyError`          | Missing dictionary key                |
| `IndexError`        | List index out of range               |
| `FileNotFoundError` | File does not exist                   |
| `ZeroDivisionError` | Division by zero                      |
| `AttributeError`    | Accessing a non-existent attribute    |
| `ImportError`       | Module not found                      |

<Tip>
  Catch the **most specific** exception you can. Catching the bare `Exception` class suppresses all errors including bugs you didn't intend to handle, making debugging much harder.
</Tip>

<Warning>
  Never silence exceptions with an empty `except` block or a bare `pass`. At minimum, log the error so you know it occurred:

  ```python theme={null}
  try:
      risky_operation()
  except Exception as e:
      print(f"Unexpected error: {e}")
      raise   # re-raise so the caller knows something went wrong
  ```
</Warning>
