Raising Exceptions & Custom Messages
try/except handles *other people's* errors. raise creates your own — the mechanism that lets your functions enforce their own rules and fail with messages that actually help.
The problem: silent wrongness
def set_age(age):
if age < 0:
print("Age can't be negative") # just... prints and continues?
return age
age = set_age(-5) # prints a warning, then returns -5 anyway!
print(age) # -5 — the bad value is loose in your programA printed warning doesn't stop anything. The invalid value flows onward, corrupting data downstream. What set_age *should* do is refuse — loudly:
raise — refusing with authority
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
return age
set_age(-5)Output:
ValueError: Age cannot be negativeraise throws the error immediately — the function stops, the invalid value never escapes, and the caller gets a clear, actionable message. You can raise any built-in exception type with a custom message:
raise ValueError("expected a number")
raise TypeError("expected a list")
raise KeyError(f"no config for {key}")
raise ZeroDivisionError("denominator was zero")Pick the type that names the *kind* of problem; use the message for the *specifics*.
The caller decides what to do
Here's the beauty of the design — the function raises, the caller chooses:
def divide(a, b):
if b == 0:
raise ValueError("denominator cannot be zero")
return a / b
# Caller 1: handle it
try:
print(divide(10, 0))
except ValueError as err:
print(f"Blocked: {err}")
# Caller 2: let it crash (maybe it's a programmer error)
print(divide(10, 0)) # full traceback — loud and proudSame function, two policies. That separation — *the function detects, the caller decides* — is the architecture of all error handling.
Validating inputs — the guard pattern
def calculate_average(marks):
if len(marks) == 0:
raise ValueError("marks list is empty")
if any(m < 0 or m > 100 for m in marks):
raise ValueError("marks must be between 0 and 100")
return sum(marks) / len(marks)
print(calculate_average([80, 90])) # 85.0
print(calculate_average([])) # ValueError: marks list is emptyGuards at the top of the function check every assumption, then the real logic runs clean. Notice the empty-list guard *before* the math — without it, sum([]) / len([]) would crash with the confusing ZeroDivisionError instead of your clear message.
When to raise vs when to return
| Situation | Choice |
|---|---|
| The input breaks the function's contract | raise — the caller must know |
| A user typed something odd | return a message / handle upstream |
| An expected alternative outcome | return normally (empty list, None, False) |
Rule of thumb: raise for programmer errors and impossible values; return for expected outcomes. int("abc") raising is correct — that's a contract violation. A search function returning an empty list is correct — "not found" is a normal result.
Common Errors & Fixes
raise "oops". Raise an exception type: raise ValueError("oops").✅ Checkpoint
raise ValueError("bad input") do to the function's execution? *(Stops it immediately)*Next: file I/O — making data outlive the program.