Python Basics - 07. Errors and Exceptions

Even the best code encounters unexpected issues: network timeouts, missing files, or invalid user inputs. Rather than having the whole script crash abruptly, Python allows you to handle these “Exceptions” gracefully.

Download Notebook

Download this notebook

1. Common Exceptions Overview

Here are a few standard exceptions you might encounter:

  • SyntaxError: Incorrect Python grammar (must be fixed before running, cannot be caught dynamically in the same block).

  • TypeError: Operation applied to an inappropriate type (e.g., adding string to int).

  • NameError: Calling a variable that hasn’t been defined.

  • ZeroDivisionError: Trying to divide by zero.

  • IndexError: Accessing an out-of-bounds index in a list.

2. Using Try-Except Blocks

Wrap potentially dangerous code inside a try block, and handle the specific error inside an except block.

try:
    # This will cause a zero division error
    result = 10 / 0
except ZeroDivisionError:
    print("Error Successfully Caught: You cannot divide a number by zero!")
except Exception as e:
    # A generic fallback for other unexpected errors
    print(f"Caught unexpected error: {e}")
Error Successfully Caught: You cannot divide a number by zero!

3. Utilizing Else and Finally

  • else: This block is executed only if no exception occurs inside the try block.

  • finally: This block is executed no matter what, regardless of whether an exception took place. It is typically used for cleaning up resources (e.g. closing network connections).

def safe_divide(x, y):
    print(f"\nAttempting to divide {x} by {y}...")
    try:
        result = x / y
    except ZeroDivisionError:
        print("[Exception] Division by zero!")
        return None
    else:
        # Runs if 'try' succeeds
        print(f"[Else] Division successful, result is: {result}")
        return result
    finally:
        # Runs in all scenarios
        print("[Finally] Execution of try-except block is complete.")

safe_divide(10, 2)
safe_divide(10, 0)
Attempting to divide 10 by 2...
[Else] Division successful, result is: 5.0
[Finally] Execution of try-except block is complete.

Attempting to divide 10 by 0...
[Exception] Division by zero!
[Finally] Execution of try-except block is complete.

4. Raising Exceptions Manually

In your own logic, you can force an error to occur if certain conditions are violated using the raise keyword.

def register_user(age):
    if age < 0:
        # We intentionally stop execution and throw our own error
        raise ValueError("A user's age cannot be negative!")
    print(f"User registered successfully with age: {age}")

try:
    register_user(-5)
except ValueError as custom_error:
    print(f"Validation Failed: {custom_error}")
Validation Failed: A user's age cannot be negative!

5. Custom Exceptions and Assertions

Custom exceptions make error handling expressive in larger systems. assert is useful during development to check assumptions, but it should not replace full runtime validation for user-facing programs.

class InvalidScoreError(Exception):
    """Raised when score is outside valid range [0, 100]."""


def normalize_score(score):
    # Validate user input before business logic.
    if not (0 <= score <= 100):
        # Raise a domain-specific exception for clearer handling.
        raise InvalidScoreError(f"Invalid score: {score}")
    return score / 100

for value in [95, 120, 75]:
    try:
        print(f"Normalized: {normalize_score(value):.2f}")
    except InvalidScoreError as e:
        # Catch specific exception type first.
        print(f"Handled custom exception: {e}")

# Assertion example
# Useful for development-time sanity checks.
x = 10
assert x > 0, "x must be positive"
print("Assertion passed")
Normalized: 0.95
Handled custom exception: Invalid score: 120
Normalized: 0.75
Assertion passed