Useful Data Tips

Python Error Handling: Best Practices and Patterns

⏱️ 32 sec read 🐍 Python

Good error handling makes your code robust and maintainable. Here's how to handle errors properly in Python:

1. Basic Try-Except

# Catch specific exceptions ✅
try:
    result = int(user_input)
except ValueError:
    print("Please enter a valid number")

# Multiple exceptions
try:
    data = json.loads(file_content)
except (json.JSONDecodeError, FileNotFoundError) as e:
    print(f"Error loading data: {e}")

2. Don't Catch All Exceptions

# Bad: Hides bugs ❌
try:
    process_data()
except:
    pass

# Bad: Too broad ❌
try:
    process_data()
except Exception:
    pass

# Good: Specific exceptions ✅
try:
    process_data()
except (ValueError, KeyError) as e:
    logger.error(f"Data processing failed: {e}")
    raise

3. Try-Except-Else-Finally

try:
    file = open('data.txt', 'r')
except FileNotFoundError:
    print("File not found")
else:
    # Runs only if no exception
    content = file.read()
    print(f"Read {len(content)} characters")
finally:
    # Always runs (cleanup)
    if 'file' in locals():
        file.close()

# Better: Use context manager
try:
    with open('data.txt', 'r') as file:
        content = file.read()
except FileNotFoundError:
    print("File not found")

4. Common Exception Types

# ValueError: Invalid value
try:
    age = int("not a number")
except ValueError as e:
    print(f"Invalid input: {e}")

# KeyError: Missing dictionary key
try:
    value = my_dict['missing_key']
except KeyError:
    value = my_dict.get('missing_key', 'default')

# TypeError: Wrong type
try:
    result = "text" + 5
except TypeError:
    result = "text" + str(5)

# FileNotFoundError: File doesn't exist
try:
    with open('missing.txt') as f:
        data = f.read()
except FileNotFoundError:
    data = "default content"

# IndexError: List index out of range
try:
    item = my_list[10]
except IndexError:
    item = None

5. Raising Exceptions

# Raise built-in exceptions
def process_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative")
    if not isinstance(age, int):
        raise TypeError("Age must be an integer")
    return age

# Re-raise with context
try:
    data = fetch_data()
except ConnectionError as e:
    logger.error("Failed to fetch data")
    raise  # Re-raises original exception

# Raise from another exception
try:
    parse_config()
except json.JSONDecodeError as e:
    raise ConfigError("Invalid config file") from e

6. Custom Exceptions

# Define custom exceptions
class DataValidationError(Exception):
    """Raised when data validation fails"""
    pass

class DatabaseConnectionError(Exception):
    """Raised when database connection fails"""
    def __init__(self, host, message="Cannot connect"):
        self.host = host
        self.message = f"{message} to {host}"
        super().__init__(self.message)

# Use custom exceptions
def validate_email(email):
    if '@' not in email:
        raise DataValidationError(f"Invalid email: {email}")
    return email

7. Practical Patterns

# Safe dictionary access
def get_user_name(user_id):
    try:
        return users[user_id]['name']
    except (KeyError, TypeError):
        return "Unknown User"

# Retry logic
def fetch_with_retry(url, max_retries=3):
    for attempt in range(max_retries):
        try:
            return requests.get(url, timeout=5)
        except requests.RequestException as e:
            if attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt)  # Exponential backoff

# Validate and convert
def safe_int(value, default=0):
    try:
        return int(value)
    except (ValueError, TypeError):
        return default

# Usage
age = safe_int(user_input, default=18)

8. Context Managers for Cleanup

# Custom context manager
from contextlib import contextmanager

@contextmanager
def database_connection(db_name):
    conn = connect_to_db(db_name)
    try:
        yield conn
    finally:
        conn.close()

# Usage
with database_connection('users') as conn:
    data = conn.query('SELECT * FROM users')
# Connection auto-closed even if error occurs

Best Practices

Pro Tip: It's easier to ask for forgiveness than permission (EAFP). In Python, use try-except rather than checking if something exists first. It's more Pythonic and often faster.

← Back to Python Tips