Python Basics — Raising Exceptions

3 minute read

Raising Exceptions

So far, you’ve been catching exceptions that Python raises automatically. But you can also raise your own exceptions to signal that something went wrong in your code.

The raise Statement

raise ValueError("Age cannot be negative")

This immediately stops execution and creates an exception that propagates up until it’s caught by a try/except block — or crashes the program.

Basic Usage

def set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative")
    if age > 150:
        raise ValueError("Age seems unrealistic")
    return age

# Works fine
print(set_age(25))  # 25

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

When to Raise vs When to Return

Use raise When:

# Input is fundamentally wrong — caller needs to know
def withdraw(account, amount):
    if amount <= 0:
        raise ValueError("Withdrawal amount must be positive")
    if amount > account.balance:
        raise ValueError(f"Insufficient funds: {account.balance} available")
    account.balance -= amount
    return account.balance

Use a return value When:

# Expected outcome — not an error condition
def find_user(users, name):
    for user in users:
        if user["name"] == name:
            return user
    return None  # Not found is normal, not exceptional

Decision Guide

Situation Use
Invalid input that should never happen raise
Violated precondition or contract raise
Missing item in a search Return None or empty
Operation might fail normally Return success/failure flag
Data integrity violation raise

Raising Built-in Exceptions

Choose the right exception type to communicate intent:

# Wrong type
def greet(name):
    if not isinstance(name, str):
        raise TypeError(f"Expected str, got {type(name).__name__}")
    return f"Hello, {name}!"

# Invalid value
def set_volume(level):
    if not 0 <= level <= 100:
        raise ValueError(f"Volume must be 0-100, got {level}")
    print(f"Volume set to {level}")

# Missing resource
def load_template(name):
    templates = {"welcome": "Hello!", "bye": "Goodbye!"}
    if name not in templates:
        raise KeyError(f"Template '{name}' not found")
    return templates[name]

# Not implemented
def process(data):
    raise NotImplementedError("Subclasses must implement process()")

Re-Raising Exceptions

Sometimes you want to catch an exception, do something (like logging), and then let it propagate:

import logging

def process_data(data):
    try:
        result = data["value"] / data["divisor"]
    except (KeyError, ZeroDivisionError) as e:
        logging.error(f"Failed to process data: {e}")
        raise  # Re-raise the same exception
    return result

The bare raise (no argument) re-raises the current exception with its original traceback intact.

Don’t Do This

# ❌ This loses the original traceback
except ValueError as e:
    raise ValueError(str(e))  # Creates a NEW exception

# ✅ This preserves the original traceback
except ValueError:
    raise  # Re-raises the original

Raising with Custom Messages

def validate_email(email):
    if not isinstance(email, str):
        raise TypeError(f"Email must be a string, got {type(email).__name__}")
    if "@" not in email:
        raise ValueError(f"Invalid email format: '{email}' (missing @)")
    if "." not in email.split("@")[1]:
        raise ValueError(f"Invalid email domain: '{email}'")
    return True

# Test
try:
    validate_email("invalid-email")
except ValueError as e:
    print(f"Validation failed: {e}")
# Validation failed: Invalid email format: 'invalid-email' (missing @)

Exception Chaining (from)

When one exception causes another, use from to preserve the chain:

def parse_config(text):
    try:
        # Simulate parsing
        pairs = text.split(",")
        config = {}
        for pair in pairs:
            key, value = pair.split("=")
            config[key.strip()] = value.strip()
        return config
    except ValueError as e:
        raise RuntimeError("Failed to parse config") from e

# Test
try:
    parse_config("key1=value1,bad_entry")
except RuntimeError as e:
    print(f"Error: {e}")
    print(f"Caused by: {e.__cause__}")

Output:

Error: Failed to parse config
Caused by: not enough values to unpack (expected 2, got 1)

Full traceback shows both exceptions:

ValueError: not enough values to unpack (expected 2, got 1)

The above exception was the direct cause of the following exception:

RuntimeError: Failed to parse config

Suppress Chaining with from None

def get_setting(config, key):
    try:
        return config[key]
    except KeyError:
        raise ValueError(f"Missing required setting: '{key}'") from None
        # The original KeyError won't appear in the traceback

# Test
try:
    get_setting({"debug": True}, "port")
except ValueError as e:
    print(e)  # Missing required setting: 'port'

Guard Clauses Pattern

Use raise early to validate inputs at the top of a function:

def create_user(name, age, email):
    # Guard clauses — validate all inputs first
    if not name or not name.strip():
        raise ValueError("Name cannot be empty")
    if not isinstance(age, int) or age < 0:
        raise ValueError(f"Age must be a positive integer, got {age}")
    if "@" not in email:
        raise ValueError(f"Invalid email: {email}")
    
    # Main logic — only reached if all inputs are valid
    user = {
        "name": name.strip(),
        "age": age,
        "email": email.lower(),
    }
    return user

# Test
try:
    user = create_user("Alice", 30, "alice@example.com")
    print(f"Created: {user}")
except ValueError as e:
    print(f"Failed: {e}")

Summary

  • Use raise to signal errors that callers must handle
  • Choose the right exception type: ValueError, TypeError, KeyError, etc.
  • Use bare raise to re-raise exceptions (preserves traceback)
  • Use from for exception chaining — shows cause and effect
  • Use from None to suppress the original exception context
  • Prefer guard clauses to validate inputs early
  • Raise exceptions for exceptional situations, not normal flow control

Next up: Custom Exceptions →

Updated: