CODE: Python Exception and Error Handling

Python errors occur when code breaks the rules of the language or runtime, while exceptions are events that disrupt normal execution. Error and exception handling allow a program to catch failures, respond safely using try-except-finally, and continue running without crashing the system.

Exceptions and Error Handling (`try`, `except`, `finally`, `raise`)

Errors are common in programming. Instead of crashing your program when something goes wrong, Python provides exceptions to handle errors gracefully.


What is an Exception?

  • An exception is an error that occurs during program execution.
  • If not handled, it will stop the program.
  • Examples:
  • ZeroDivisionError → dividing by zero.
  • FileNotFoundError → file doesn’t exist.
  • ValueError → invalid data type.

python
print(10 / 0)  # ZeroDivisionError


2. Basic `try` and `except`

Wrap risky code in try. If an error occurs, control jumps to except.

python
try:
    num = int("abc")
except ValueError:
    print("Conversion failed: invalid number")


Handling Multiple Exceptions

You can handle different errors separately.

python
try:
    f = open("data.txt", "r")
    result = 10 / 0
except FileNotFoundError:
    print("File not found")
except ZeroDivisionError:
    print("You cannot divide by zero")


Catching All Exceptions

You can use a generic except Exception as e to catch any error.

python
try:
    value = int("oops")
except Exception as e:
    print("Error:", e)


Using `finally`

The finally block always runs, whether an error occurs or not.

Useful for cleanup tasks (closing files, releasing resources).

python
try:
    f = open("example.txt", "r")
    data = f.read()
except FileNotFoundError:
    print("File missing")
finally:
    print("Closing program")


Using `else`

The else block runs only if no exception occurs.

python
try:
    x = 5 / 1
except ZeroDivisionError:
    print("Division error")
else:
    print("Success, result is", x)


Raising Exceptions (`raise`)

You can throw your own exceptions with raise.

python
def withdraw(amount):
    if amount < 0:
        raise ValueError("Amount must be positive")
    print("Withdrawing:", amount)

withdraw(-100)  # Raises ValueError


Custom Exceptions

You can define your own error types by subclassing Exception.

python
class NegativeValueError(Exception):
    pass

def set_age(age):
    if age < 0:
        raise NegativeValueError("Age cannot be negative")
    print("Age set to", age)

set_age(-5)  # Raises NegativeValueError


Creating Custom Exceptions

Built-in exceptions cover many cases, but larger programs benefit from domain-specific errors that are easier to understand, catch, and test. Custom exceptions are just classes that inherit from Exception (or one of its subclasses).


Why create custom exceptions?

  • Clarity: InsufficientFundsError is more informative than ValueError.
  • Selective handling: Catch only your library’s failures without masking others.
  • Richer context: Attach fields (e.g., account id, current balance) for debugging.

Defining a custom exception (the minimal way)

python
class InsufficientFundsError(Exception):
    """Raised when an account withdrawal exceeds available balance."""
    pass

Raise it where appropriate:

python
def withdraw(balance: float, amount: float):
    if amount > balance:
        raise InsufficientFundsError(f"amount={amount} > balance={balance}")
    return balance - amount

Catch it precisely:

python
try:
    withdraw(100, 150)
except InsufficientFundsError as e:
    print("Withdrawal failed:", e)


Give your exception fields and a nice __str__:

python
class RateLimitExceeded(Exception):
    """API rate limit exceeded."""
    def __init__(self, limit: int, window_s: int, retry_after_s: int):
        self.limit = limit
        self.window_s = window_s
        self.retry_after_s = retry_after_s
        super().__init__(f"Limit {limit}/{window_s}s; retry after {retry_after_s}s")

# Usage
try:
    raise RateLimitExceeded(limit=100, window_s=60, retry_after_s=12)
except RateLimitExceeded as e:
    print(e.limit, e.retry_after_s)  # structured access

Tip: Keep the message human-readable and expose fields for code.


Build an exception hierarchy

Create a base exception for your package, then derive specifics. This lets users catch your errors collectively or individually.

python
class VarPayError(Exception):  # package-wide base
    """Base error for VarPay SDK."""

class AuthError(VarPayError): ...
class CardDeclinedError(VarPayError): ...
class NetworkError(VarPayError): ...

Catching options:

python
try:
    # code calling VarPay
    ...
except CardDeclinedError:
    # specific handling
    ...
except VarPayError as e:
    # fallback for all SDK errors
    ...


Exception chaining (`raise ... from ...`)

Use raise X from Y to preserve root cause while providing higher-level context.

python
class ConfigLoadError(Exception): ...

def load_config(path):
    try:
        with open(path) as f:
            return f.read()
    except OSError as e:
        raise ConfigLoadError(f"Failed to load config: {path}") from e

Now tracebacks show both ConfigLoadError and the original OSError.


Best practices

  • Inherit from Exception, not BaseException (reserved for SystemExitKeyboardInterrupt, etc.).
  • Name ends with Error (convention).
  • Keep messages actionable (what failed + key parameters).
  • Don’t overuse: prefer built-ins when they clearly fit.
  • Avoid blanket except Exception: unless you re-raise; never swallow errors silently.
  • Document when/why they’re raised (docstrings).

Example: validating inputs with custom errors

python
class ValidationError(Exception): ...

class MissingFieldError(ValidationError):
    def __init__(self, field: str):
        self.field = field
        super().__init__(f"Missing required field: {field}")

class RangeError(ValidationError):
    def __init__(self, field: str, minv, maxv, got):
        self.field, self.minv, self.maxv, self.got = field, minv, maxv, got
        super().__init__(f"{field} must be in [{minv}, {maxv}] (got {got})")

def validate_user(payload: dict):
    if "age" not in payload:
        raise MissingFieldError("age")
    if not (0 <= payload["age"] <= 120):
        raise RangeError("age", 0, 120, payload["age"])
    return True

try:
    validate_user({"age": 150})
except ValidationError as e:
    # Handles both MissingFieldError and RangeError
    print("Validation failed:", e)


Cleanup with `finally` and re-raising

python
class UploadError(Exception): ...

def upload(file):
    conn = open_connection()
    try:
        conn.send(file)
    except NetworkError as e:
        # wrap with context, keep cause
        raise UploadError("Upload failed") from e
    finally:
        conn.close()  # always executed


Summary

  • Create exceptions by subclassing Exception.
  • Provide clear names, docstrings, and structured context (fields).
  • Organize a hierarchy with a package-level base error.
  • Use raise ... from ... to preserve the original cause.
  • Catch narrowly; avoid silencing unexpected failures.