File Handling & Modules

Python Error Handling

Learn how to handle exceptions in Python using try-except, raise custom exceptions, and write robust error-handling code.

Python Error Handling

Error handling allows your program to gracefully handle unexpected situations instead of crashing. Python uses exceptions — objects that represent errors — and the try-except mechanism to catch and handle them.


Types of Errors

Syntax Errors (Caught Before Execution)

python
# Missing colon
# if True
#     print("error")  # SyntaxError

# Unclosed bracket
# data = [1, 2, 3  # SyntaxError

Exceptions (Caught During Execution)

python
# Common exceptions
print(10 / 0)              # ZeroDivisionError
print(int("abc"))           # ValueError
print([1, 2][5])            # IndexError
print({"a": 1}["b"])        # KeyError
print(undefined_var)         # NameError
print("hello" + 5)          # TypeError

try-except

python
try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero!")

# Program continues...
print("Still running!")

Catch Specific Exceptions

python
try:
    number = int(input("Enter a number: "))
    result = 100 / number
    print(f"Result: {result}")
except ValueError:
    print("That's not a valid number!")
except ZeroDivisionError:
    print("Cannot divide by zero!")

Catch Multiple Exceptions

python
try:
    value = int("abc")
except (ValueError, TypeError) as e:
    print(f"Error: {e}")

Catch All Exceptions

python
try:
    risky_operation()
except Exception as e:
    print(f"Something went wrong: {type(e).__name__}: {e}")

# Bare except (catches EVERYTHING - avoid this)
try:
    risky_operation()
except:  # Not recommended - catches KeyboardInterrupt, SystemExit, etc.
    print("Error occurred")

Best Practice: Always catch specific exceptions. Avoid bare except: clauses.


try-except-else-finally

python
try:
    number = int(input("Enter a number: "))
    result = 100 / number
except ValueError:
    print("Invalid input!")
except ZeroDivisionError:
    print("Cannot divide by zero!")
else:
    # Runs only if NO exception occurred
    print(f"Result: {result}")
finally:
    # ALWAYS runs, whether exception occurred or not
    print("Execution complete")

finally for Cleanup

python
file = None
try:
    file = open("data.txt", "r")
    content = file.read()
    process(content)
except FileNotFoundError:
    print("File not found!")
except Exception as e:
    print(f"Error: {e}")
finally:
    if file:
        file.close()  # Always close the file
    print("Cleanup done")

# Better: use context manager
with open("data.txt", "r") as file:
    content = file.read()
# Automatically closed, even on error

Raising Exceptions

Use raise to throw an exception:

python
def set_age(age):
    if not isinstance(age, int):
        raise TypeError("Age must be an integer")
    if age < 0:
        raise ValueError("Age cannot be negative")
    if age > 150:
        raise ValueError("Age cannot exceed 150")
    return age

try:
    set_age(-5)
except ValueError as e:
    print(f"Invalid age: {e}")
# Invalid age: Age cannot be negative

# Re-raise an exception
try:
    set_age("abc")
except TypeError:
    print("Logging error...")
    raise  # Re-raises the same exception

Custom Exceptions

Create your own exception classes by inheriting from Exception:

python
class AppError(Exception):
    """Base exception for the application."""
    pass

class ValidationError(AppError):
    """Raised when validation fails."""
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"Validation error on '{field}': {message}")

class AuthenticationError(AppError):
    """Raised when authentication fails."""
    def __init__(self, username):
        self.username = username
        super().__init__(f"Authentication failed for user '{username}'")

class NotFoundError(AppError):
    """Raised when a resource is not found."""
    pass


# Usage
def login(username, password):
    if not username:
        raise ValidationError("username", "cannot be empty")
    if len(password) < 8:
        raise ValidationError("password", "must be at least 8 characters")
    if username != "admin" or password != "secret123":
        raise AuthenticationError(username)
    return {"username": username, "role": "admin"}


try:
    user = login("admin", "short")
except ValidationError as e:
    print(f"Validation: {e.field} - {e.message}")
except AuthenticationError as e:
    print(f"Auth failed: {e.username}")
except AppError as e:
    print(f"App error: {e}")

Exception Hierarchy

Python's built-in exception hierarchy:

BaseException
├── KeyboardInterrupt
├── SystemExit
├── GeneratorExit
└── Exception
    ├── ArithmeticError
    │   ├── ZeroDivisionError
    │   ├── OverflowError
    │   └── FloatingPointError
    ├── AttributeError
    ├── ImportError
    │   └── ModuleNotFoundError
    ├── LookupError
    │   ├── IndexError
    │   └── KeyError
    ├── NameError
    ├── OSError
    │   ├── FileNotFoundError
    │   ├── PermissionError
    │   └── FileExistsError
    ├── TypeError
    ├── ValueError
    └── RuntimeError
        └── RecursionError

Common Exception Patterns

LBYL vs EAFP

python
# LBYL: Look Before You Leap (check first)
if key in dictionary:
    value = dictionary[key]
else:
    value = default

# EAFP: Easier to Ask Forgiveness than Permission (try first)
try:
    value = dictionary[key]
except KeyError:
    value = default

# Python community prefers EAFP

Retry Pattern

python
def retry(func, max_attempts=3):
    """Retry a function up to max_attempts times."""
    for attempt in range(1, max_attempts + 1):
        try:
            return func()
        except Exception as e:
            if attempt == max_attempts:
                raise
            print(f"Attempt {attempt} failed: {e}")

Exception Chaining

python
try:
    data = {"key": "not_a_number"}
    value = int(data["key"])
except ValueError as e:
    raise RuntimeError("Failed to process data") from e
# RuntimeError: Failed to process data
# The above exception was the direct cause of:
# ValueError: invalid literal for int()

Practical Example: Safe Data Processor

python
"""
A robust data processor with proper error handling.
"""

class DataProcessor:
    def __init__(self):
        self.errors = []

    def process_record(self, record):
        """Process a single record with error handling."""
        try:
            name = record["name"]
            age = int(record["age"])
            score = float(record["score"])

            if age < 0 or age > 150:
                raise ValueError(f"Invalid age: {age}")
            if score < 0 or score > 100:
                raise ValueError(f"Invalid score: {score}")

            return {
                "name": name.title(),
                "age": age,
                "score": score,
                "grade": self._calculate_grade(score)
            }

        except KeyError as e:
            self.errors.append(f"Missing field {e} in record: {record}")
        except (ValueError, TypeError) as e:
            self.errors.append(f"Invalid data in record {record}: {e}")
        return None

    def _calculate_grade(self, score):
        if score >= 90: return "A"
        if score >= 80: return "B"
        if score >= 70: return "C"
        if score >= 60: return "D"
        return "F"

    def process_all(self, records):
        """Process all records, collecting results and errors."""
        results = []
        for record in records:
            result = self.process_record(record)
            if result:
                results.append(result)
        return results


# Usage
processor = DataProcessor()
raw_data = [
    {"name": "alice", "age": "25", "score": "92"},
    {"name": "bob", "age": "bad", "score": "85"},      # ValueError
    {"name": "charlie", "score": "78"},                  # Missing age
    {"name": "diana", "age": "22", "score": "95"},
    {"name": "eve", "age": "30", "score": "200"},        # Invalid score
]

results = processor.process_all(raw_data)

print("Processed Records:")
for r in results:
    print(f"  {r['name']}: Grade {r['grade']} (score: {r['score']})")

print(f"\nErrors ({len(processor.errors)}):")
for error in processor.errors:
    print(f"  ⚠ {error}")

Summary

  • Use try-except to catch and handle exceptions gracefully
  • Catch specific exceptions — avoid bare except: clauses
  • else runs when no exception occurs; finally always runs
  • raise to throw exceptions; raise ... from for exception chaining
  • Create custom exceptions by inheriting from Exception
  • Python prefers EAFP (try first) over LBYL (check first)
  • Use context managers (with) for automatic resource cleanup
  • Always handle exceptions at the appropriate level of your application

Next, we'll learn about Python generators and iterators.