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 = percentNow 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.
raiseforces 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 / bTests
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)