← Home

077. Raising Exceptions

Signal errors explicitly with raise instead of silent wrong behavior

077. Raising Exceptions

He passes threshold=-5 to is_ram_critical() by accident. The function happily returns True for every RAM value since everything is above -5. The bug is invisible.

The fix: validate at the function boundary and raise immediately.

def set_threshold(percent: float) -> None:
    if not (0 <= percent <= 100):
        raise ValueError(
            f"Threshold must be 0–100, got {percent}"
        )
    global THRESHOLD
    THRESHOLD = percent

Now set_threshold(-5) explodes loudly at the call site — not silently hours later when every alert fires wrong.

💡 Rule: fail fast, fail loud. Returning an error string lets callers ignore it. raise forces them to deal with it. Every major API you’ll ever use (requests, psutil, boto3) raises exceptions for bad inputs — never returns "error: ..." strings.

Learning objectives

  • Raise exceptions using the raise statement
  • Choose appropriate built-in exception types
  • Understand why raise is better than returning error strings

Key concepts

  • raise
  • ValueError
  • ZeroDivisionError
  • exception types

Try it

Concept detail

raise ExceptionType(“message”) signals that something went wrong.

raise ValueError("Age must be 0-150, got -5")
raise TypeError(f"Expected int, got {type(x).__name__}")
raise ZeroDivisionError("Cannot divide by zero")

What happens when you raise:

  • Execution stops at the raise statement
  • The exception propagates up the call stack
  • Each caller either handles it (try/except) or lets it keep propagating
  • If nothing handles it, Python prints the traceback and exits

WHY raise instead of returning an error string: # Bad — caller can ignore it: result = validate_age(-1) # returns “Invalid age” store(result) # stores “Invalid age” silently

# Good — caller is forced to handle it:
validate_age(-1)             # raises ValueError
# execution never reaches store()

Built-in exception types: ValueError — bad value (wrong range, format) TypeError — wrong type (passed string, expected int) KeyError — missing dict key IndexError — list index out of range ZeroDivisionError — division by zero FileNotFoundError — file doesn’t exist AttributeError — object has no such attribute

Raise with or without message: raise ValueError() # valid, but unhelpful raise ValueError(“got -5”) # include context — saves debugging time

Custom exceptions: class RamThresholdError(ValueError): pass raise RamThresholdError(“Threshold -5 is out of range 0-100”)

Solution

def validate_age(age):
    if age < 0 or age > 150:
        raise ValueError(f"Age must be 0-150, got {age}")
    return age

def validate_username(username):
    if len(username) < 3:
        raise ValueError(f"Username too short: {len(username)} chars (min 3)")
    if len(username) > 20:
        raise ValueError(f"Username too long: {len(username)} chars (max 20)")
    return username

def divide(a, b):
    if b == 0:
        raise ZeroDivisionError("Cannot divide by zero")
    return a / b

Tests

def test_validate_age_valid():
    assert validate_age(25) == 25
    assert validate_age(0) == 0
    assert validate_age(150) == 150

def test_validate_age_negative():
    with pytest.raises(ValueError):
        validate_age(-1)

def test_validate_age_too_high():
    with pytest.raises(ValueError):
        validate_age(200)

def test_validate_username_valid():
    assert validate_username("alice") == "alice"
    assert validate_username("a" * 20) == "a" * 20

def test_validate_username_short():
    with pytest.raises(ValueError):
        validate_username("ab")

def test_validate_username_long():
    with pytest.raises(ValueError):
        validate_username("a" * 21)

def test_divide_normal():
    assert divide(10, 2) == 5.0

def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

Resources