Raising your own exceptions
So far you have handled errors other people's code raised. Now you raise your own — so your functions can refuse bad input loudly instead of quietly producing a wrong answer.
raise
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
return age
set_age(-5)
Traceback (most recent call last):
File "app.py", line 6, in <module>
set_age(-5)
File "app.py", line 3, in set_age
raise ValueError("Age cannot be negative")
ValueError: Age cannot be negative
raise stops the function immediately — like return, but signalling failure
rather than producing a result. The caller either handles it or the program
stops.
Why this is better than returning an error
Module 5 flagged a function that returned a number sometimes and a string others. Compare the options:
def set_age(age):
if age < 0:
return None # caller must remember to check
def set_age(age):
if age < 0:
return "Error" # caller might do arithmetic on it
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
Only the third cannot be ignored. A caller who forgets to check a None
gets a confusing TypeError twenty lines later, in a function that did nothing
wrong. A caller who ignores an exception gets a clear traceback pointing
straight at the actual problem.
This is the important idea: fail where the problem is, not where the symptom appears. An exception at the point of bad input is far cheaper to debug than a wrong value travelling through three functions before breaking something else.
Returning None is still fine when "not found" is a normal, expected outcome —
find_user returning None is reasonable, and dict | None documents it.
Raising is for input that is genuinely wrong.
Choosing the type
Use the built-in that fits:
raise ValueError("Age cannot be negative") # right type, bad value
raise TypeError("Expected a list, got a string") # wrong type entirely
raise KeyError("email") # missing key
raise FileNotFoundError("config.json not found")
raise NotImplementedError("Not written yet")
ValueError covers most validation. Reach for TypeError only when the type is
genuinely wrong, not merely the value.
Avoid raise Exception("..."). It is the raising equivalent of a bare
except: — callers cannot catch it selectively without catching everything.
Write messages for whoever has to fix it
raise ValueError("Invalid input")
raise ValueError(f"Age must be between 0 and 150, got {age}")
The second names the rule and the offending value, so the traceback alone usually solves the problem. Error messages are documentation that only appears when it is needed — spend the extra ten seconds.
Validating at the edges
The most useful place to raise is where data enters your program:
def create_user(name: str, email: str, age: int) -> dict:
"""Create a user record.
Raises:
ValueError: If any field is missing or out of range.
"""
if not name.strip():
raise ValueError("Name cannot be empty")
if "@" not in email:
raise ValueError(f"Not a valid email address: {email!r}")
if not 0 < age < 150:
raise ValueError(f"Age must be between 1 and 149, got {age}")
return {"name": name.strip(), "email": email.lower(), "age": age}
Guard clauses again, now enforcing rules. Past those checks, the rest of the function can assume its inputs are good — which is what keeps the body simple.
Note {email!r} — the !r uses repr(), so the value appears quoted. That
matters when the problem is an invisible space or an empty string, which print
identically to nothing at all.
Note also the Raises: line in the docstring. A caller cannot see what a
function throws from its signature, so that is the only place to record it.
Your own exception types
When you want callers to handle your failures specifically, define a type:
class InsufficientStockError(Exception):
"""Raised when an order asks for more than is available."""
def reserve(item: str, quantity: int, available: int) -> None:
if quantity > available:
raise InsufficientStockError(
f"Requested {quantity} of {item}, only {available} available"
)
Classes are module 9, and you can use this now without understanding the
machinery: a line naming your exception, (Exception) after it, and a docstring
as the body.
The reason to bother is that callers can single it out:
try:
reserve("rice", 10, 3)
except InsufficientStockError:
suggest_alternatives()
except ValueError:
show_form_error()
With ValueError for both, that distinction would be impossible.
Do not create one per error. A handful of meaningful types for an application is plenty; the built-ins cover the rest.
Re-raising
Sometimes you want to react to an error without swallowing it:
try:
process(order)
except ValueError:
log("Order processing failed")
raise
A bare raise inside except re-raises the exception you caught, with its
original traceback intact. Logging and letting it continue upward is a common
and correct pattern.
Compare with raising a new one, which loses the original context unless you chain it:
try:
config = json.loads(text)
except json.JSONDecodeError as error:
raise ValueError(f"Could not read config: {error}") from error
from error produces the "direct cause of the following exception" traceback
from the reading lesson — the caller sees a meaningful error and can still see
what actually broke. Without from, you would be throwing away the only clue.
assert is not for this
assert age > 0, "Age must be positive"
That raises AssertionError when false, and looks like a neat shortcut. It is
not, for one reason: assertions are removed entirely when Python runs with
-O. Validation that vanishes in production is worse than none.
Use assert for checking your own assumptions during development, and in tests
— which is exactly how the refactor lesson used it. Use raise for anything
that validates real input.
Practice
- Write
set_age(age)raisingValueErrorfor negatives. Call it and read the traceback. - Rewrite it three ways — returning
None, returning a string, raising — and write a careless caller for each. Which failures are hardest to diagnose? - Write
create_userwith the three validations. Test each failing case. - Improve a vague message like
"Invalid input"to name the rule and show the value. Use!rand test it with" ". - Define
InsufficientStockErrorand use it inreserve. Write a caller that handles it differently fromValueError. - Write a function that logs and re-raises with a bare
raise. Confirm the original traceback survives. - Use
raise ... from ...to wrap aValueErrorfromint()in something more meaningful. Read both halves of the resulting traceback. - Add
Raises:sections to the docstrings of everything above. - Explain in one sentence why
assertshould not validate user input.
Next: finding bugs that do not raise anything at all.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship