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.
print(10 / 0) # ZeroDivisionError
2. Basic `try` and `except`
Wrap risky code in try. If an error occurs, control jumps to except.
try:
num = int("abc")
except ValueError:
print("Conversion failed: invalid number")
Handling Multiple Exceptions
You can handle different errors separately.
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.
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).
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.
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.
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.
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:
InsufficientFundsErroris more informative thanValueError. - 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)
class InsufficientFundsError(Exception):
"""Raised when an account withdrawal exceeds available balance."""
pass
Raise it where appropriate:
def withdraw(balance: float, amount: float):
if amount > balance:
raise InsufficientFundsError(f"amount={amount} > balance={balance}")
return balance - amount
Catch it precisely:
try:
withdraw(100, 150)
except InsufficientFundsError as e:
print("Withdrawal failed:", e)
Adding structured context (recommended)
Give your exception fields and a nice __str__:
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.
class VarPayError(Exception): # package-wide base
"""Base error for VarPay SDK."""
class AuthError(VarPayError): ...
class CardDeclinedError(VarPayError): ...
class NetworkError(VarPayError): ...
Catching options:
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.
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, notBaseException(reserved forSystemExit,KeyboardInterrupt, 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
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
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.