Lesson 7 of 9
So far you've caught exceptions Python raised. You can raise them yourself with raise — that's
how a function reports "you gave me something I can't work with."
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
return age
print(set_age(30))
print(set_age(-5))
Why raise instead of returning an error?
Compare the two approaches:
def withdraw(balance, amount):
if amount > balance:
return "Insufficient funds"
return balance - amount
result = withdraw(100, 500)
new_balance = result - 10
Returning a string means the caller can ignore the problem, and result - 10 blows up much
later with a confusing TypeError far from the real cause.
def withdraw(balance, amount):
if amount > balance:
raise ValueError("Insufficient funds")
return balance - amount
try:
print(withdraw(100, 500))
except ValueError as e:
print(f"Rejected: {e}")
An exception can't be silently ignored — the caller either handles it or the program stops with a traceback pointing at the real problem.
Rough guideline: return a value for expected outcomes the caller will inspect anyway
(.get() returning None), raise when the function genuinely cannot do its job.
Pick a meaningful type
Use the exception that describes the problem:
def get_item(items, index):
if not isinstance(items, list):
raise TypeError("items must be a list")
if index >= len(items):
raise IndexError(f"index {index} out of range")
return items[index]
for bad in [lambda: get_item("abc", 0), lambda: get_item([1, 2], 5)]:
try:
bad()
except (TypeError, IndexError) as e:
print(f"{type(e).__name__}: {e}")
ValueError for a bad value, TypeError for a wrong type, KeyError for a missing key. A
specific type lets callers catch precisely what they care about.
Validating early
Check inputs at the top of a function and raise immediately. This is often called a guard clause, and it keeps the rest of the body free of defensive nesting:
def create_user(name, age):
if not name:
raise ValueError("Name is required")
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0 or age > 150:
raise ValueError(f"Age {age} is out of range")
return {"name": name, "age": age}
print(create_user("Ada", 36))
try:
create_user("", 36)
except ValueError as e:
print(e)
Re-raising
Sometimes you want to react to an exception but still let it propagate. A bare raise inside
except re-raises the original, traceback intact:
def load(path):
try:
with open(path) as f:
return f.read()
except FileNotFoundError:
print(f"Logging: {path} not found")
raise
try:
load("nope.txt")
except FileNotFoundError:
print("Caller handled it.")
Write raise on its own here, not raise e — the bare form preserves the original traceback.
raise ... from
When one failure causes another, from records the connection so the traceback shows both:
def parse_config(text):
try:
return int(text)
except ValueError as e:
raise ValueError(f"Invalid config value: {text!r}") from e
try:
parse_config("abc")
except ValueError as e:
print(e)
print(type(e.__cause__).__name__)
Try it yourself
Write a validator that raises the right exception type for each problem.
Checkpoint
raise SomeError("message")signals failure from your own code.- Raising beats returning an error string — it can't be silently ignored.
- Match the exception type to the problem:
ValueError,TypeError,KeyError. - Validate inputs early with guard clauses.
- A bare
raisere-raises and keeps the traceback;raise ... from elinks causes.
Questions about this lesson
Log in to ask a question.