Python Tutorials

Choose a lesson to begin

Error Handling and Exceptions

Errors happen. Good programs handle them gracefully instead of crashing. Python's exception system lets you catch errors, recover from them, and provide helpful feedback to users.

Why Error Handling Matters

Without error handling:

age = int(input("Enter age: "))  # User types "twenty"

ValueError: invalid literal for int() with base 10: 'twenty'

Program crashes!

With error handling:

try:
    age = int(input("Enter age: "))
except ValueError:
    print("Please enter a number!")
    age = None

Program continues running

Basic Try-Except

try:
    # Code that might fail
    number = int("abc")
except ValueError:
    # Runs if ValueError occurs
    print("Can't convert 'abc' to integer!")

Catching Multiple Exceptions

try:
    numbers = [1, 2, 3]
    index = int(input("Enter index: "))
    print(numbers[index])
except ValueError:
    print("Please enter a valid number!")
except IndexError:
    print(f"Index out of range (0-{len(numbers)-1})")

Catching Any Exception

try:
    # Risky operation
    result = risky_function()
except Exception as e:
    # Catches ALL exceptions
    print(f"Error: {e}")
    print(f"Error type: {type(e).__name__}")

Warning: Catching Exception catches almost everything. Usually better to catch specific exceptions.

Multiple Exceptions, One Handler

try:
    # Code that could fail in multiple ways
    value = data[key] / divisor
except (KeyError, ZeroDivisionError, TypeError) as e:
    print(f"Operation failed: {e}")

Getting Exception Details

try:
    number = int("not a number")
except ValueError as e:
    print(f"Error message: {e}")
    print(f"Error type: {type(e).__name__}")
    
    # Full traceback details
    import traceback
    print(traceback.format_exc())

The else Clause

Runs if NO exception occurred:

try:
    number = int(input("Enter a number: "))
    result = 100 / number
except ValueError:
    print("Invalid input!")
except ZeroDivisionError:
    print("Cannot divide by zero!")
else:
    # Only runs if try succeeded
    print(f"Result: {result}")
    print("Calculation successful!")

The finally Clause

Always runs, whether exception occurred or not:

try:
    file = open("data.txt", "r")
    data = file.read()
except FileNotFoundError:
    print("File not found!")
    data = None
finally:
    # Always runs - clean up resources
    if 'file' in locals():
        file.close()
    print("Cleanup complete")

Complete Try-Except Structure

try:
    # Try to do something risky
    result = process_data()
except SpecificError:
    # Handle specific error
    handle_error()
except AnotherError as e:
    # Handle another type
    print(f"Error: {e}")
else:
    # Only if try succeeded
    print("Success!")
finally:
    # Always runs
    cleanup()

Common Built-in Exceptions

# ValueError - invalid value for type
int("abc")  # ValueError

TypeError - wrong type

"hello" + 5 # TypeError

KeyError - missing dictionary key

d = {"a": 1} d["b"] # KeyError

IndexError - invalid list index

lst = [1, 2, 3] lst[10] # IndexError

AttributeError - invalid attribute

"hello".nonexistent # AttributeError

FileNotFoundError - file doesn't exist

open("missing.txt") # FileNotFoundError

ZeroDivisionError - division by zero

10 / 0 # ZeroDivisionError

ImportError - can't import module

import nonexistent_module # ImportError

Raising Exceptions

Throw your own errors:

def set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative!")
    if age > 150:
        raise ValueError("Age seems unrealistic!")
    return age
try:
    set_age(-5)
except ValueError as e:
    print(f"Invalid age: {e}")

Re-raising Exceptions

def process_file(filename):
    try:
        with open(filename) as f:
            data = f.read()
    except FileNotFoundError:
        print(f"Logging: {filename} not found")
        raise  # Re-raise same exception
    return data
try:
    process_file("missing.txt")
except FileNotFoundError:
    print("Handled at higher level")

Custom Exceptions

Create your own exception types:

class InvalidEmailError(Exception):
    """Raised when email format is invalid."""
    pass
class PasswordTooShortError(Exception):
    """Raised when password is too short."""
    def __init__(self, length, minimum=8):
        self.length = length
        self.minimum = minimum
        super().__init__(
            f"Password is {length} characters, minimum is {minimum}"
        )
def validate_email(email):
    if "@" not in email:
        raise InvalidEmailError(f"Invalid email: {email}")
def validate_password(password):
    if len(password) < 8:
        raise PasswordTooShortError(len(password))
try:
    validate_email("notanemail")
except InvalidEmailError as e:
    print(f"Error: {e}")
try:
    validate_password("short")
except PasswordTooShortError as e:
    print(f"Error: {e}")
    print(f"Need {e.minimum - e.length} more characters")

Context Managers (with statement)

Automatically handle setup and cleanup:

# Without context manager - manual cleanup
file = open("data.txt", "w")
try:
    file.write("Hello")
finally:
    file.close()  # Must remember to close

With context manager - automatic cleanup

with open("data.txt", "w") as file: file.write("Hello")

File automatically closed, even if error occurs!

Multiple resources

with open("input.txt") as infile, open("output.txt", "w") as outfile: data = infile.read() outfile.write(data.upper())

Creating Your Own Context Manager

class Timer:
    def __enter__(self):
        """Called when entering 'with' block."""
        import time
        self.start = time.time()
        print("Timer started")
        return self   
    def __exit__(self, exc_type, exc_val, exc_tb):
        """Called when exiting 'with' block."""
        import time
        elapsed = time.time() - self.start
        print(f"Timer stopped. Elapsed: {elapsed:.2f}s")
        # Return False to propagate exceptions
        return False

Use it

with Timer(): # Your code here sum(range(1000000))

Timer started

Timer stopped. Elapsed: 0.03s

Assertions

Check conditions during development:

def calculate_average(numbers):
    assert len(numbers) > 0, "List cannot be empty"
    assert all(isinstance(n, (int, float)) for n in numbers), "All items must be numbers"
    return sum(numbers) / len(numbers)

Assertions enabled

calculate_average([]) # AssertionError: List cannot be empty

Assertions can be disabled with -O flag

# python -O script.py

Use assertions for:

Don't use assertions for:

Real-World Examples

Robust User Input

def get_positive_number(prompt):
    """Keep asking until valid positive number entered."""
    while True:
        try:
            value = float(input(prompt))
            if value <= 0:
                print("Please enter a positive number!")
                continue
            return value
        except ValueError:
            print("Invalid input! Please enter a number.")
age = get_positive_number("Enter your age: ")

File Processing with Error Handling

def process_file(filename):
    """Process file with comprehensive error handling."""
    try:
        with open(filename, 'r') as f:
            data = f.read()
    except FileNotFoundError:
        print(f"Error: '{filename}' not found")
        return None
    except PermissionError:
        print(f"Error: No permission to read '{filename}'")
        return None
    except Exception as e:
        print(f"Unexpected error reading '{filename}': {e}")
        return None    
    # Process data
    try:
        result = process_data(data)
    except ValueError as e:
        print(f"Error processing data: {e}")
        return None  
    return result

API Request with Retry

import time

def fetch_data(url, max_retries=3): """Fetch data with automatic retry on failure.""" for attempt in range(max_retries): try: response = make_request(url) return response except ConnectionError as e: if attempt < max_retries - 1: wait_time = 2 ** attempt # Exponential backoff print(f"Connection failed. Retrying in {wait_time}s...") time.sleep(wait_time) else: print(f"Failed after {max_retries} attempts") raise except Timeout: print("Request timed out") return None

Database Transaction

class DatabaseError(Exception):
    pass
def execute_transaction(operations):
    """Execute multiple database operations atomically."""
    try:
        db.begin_transaction()       
        for operation in operations:
            operation.execute()        
        db.commit()
        print("Transaction successful")        
    except DatabaseError as e:
        db.rollback()
        print(f"Transaction failed: {e}")
        raise
    finally:
        db.close_connection()

Validation Class

class ValidationError(Exception):
    """Base class for validation errors."""
    pass
class EmailValidationError(ValidationError):
    pass
class AgeValidationError(ValidationError):
    pass
class UserValidator:
    @staticmethod
    def validate_email(email):
        if not isinstance(email, str):
            raise EmailValidationError("Email must be a string")
        if "@" not in email or "." not in email:
            raise EmailValidationError(f"Invalid email format: {email}")   
    @staticmethod
    def validate_age(age):
        if not isinstance(age, int):
            raise AgeValidationError("Age must be an integer")
        if age < 0 or age > 150:
            raise AgeValidationError(f"Age out of valid range: {age}")   
    @classmethod
    def validate_user(cls, email, age):
        """Validate all user data."""
        errors = []      
        try:
            cls.validate_email(email)
        except EmailValidationError as e:
            errors.append(str(e))     
        try:
            cls.validate_age(age)
        except AgeValidationError as e:
            errors.append(str(e))      
        if errors:
            raise ValidationError(f"Validation failed: {', '.join(errors)}")

Use it

try: UserValidator.validate_user("invalid", -5) except ValidationError as e: print(f"User validation failed: {e}")

Best Practices

  • Be specific: Catch specific exceptions, not Exception
  • Don't hide errors: Only catch what you can handle
  • Clean up resources: Use finally or with
  • Provide context: Include helpful error messages
  • Log errors: Record what went wrong for debugging
  • Fail fast: Validate early, raise exceptions quickly
  • Document exceptions: Tell users what exceptions your code raises
  • Try It Out!

  • Write a function that safely converts strings to integers
  • Create a custom exception for invalid game moves
  • Build a file reader that handles missing files gracefully
  • Implement a retry decorator for network operations
  • Write a context manager for timing code execution
  • Key Takeaways

    Error handling makes your programs robust and user-friendly! 🐍

    🐍 Python Runner