Definition
Picture a function that divides two numbers. The calculation is trivial, except when the divisor is zero. Carrying on would then produce a confusing crash further down, once that impossible result gets reused somewhere else. The right answer is to stop right there, exactly where things go wrong, and say so clearly.
raise exists for exactly that: the statement triggers an exception on purpose, execution stops at that line, and the error travels up the call stack exactly as if Python had raised it itself. It is what gives code the right to refuse to carry on.
Here is that idea applied to division:
def divide(a, b):
if b == 0:
# Stop right here, not three lines further down
raise ValueError("The divisor cannot be zero")
return a / bWhy raise rather than return a value
Faced with a situation it cannot handle, a function really has two possible paths: return a value reporting the failure, a -1 or a None the caller is supposed to check, or raise an exception. These two paths are not equivalent.
The trouble with the first path is that it relies on the caller's discipline. Nothing forces them to check the return value, and in practice, they will not always check it. Compare these two versions of the same idea:
# The caller can ignore the -1, and will
def find_client(identifier):
...
return -1
# The caller cannot ignore this
def find_client(identifier):
...
raise LookupError(f"Client {identifier} not found")The fallback value produces bugs that surface far from their cause: the -1 returned to signal a failure ends up serving as a real identifier, and the error only shows up much later, on entirely different data. The exception stops at the source and names the problem right away.
Choosing the right type
Raising an exception is not enough on its own: you also need to pick the right type, and that choice is not cosmetic. It is what lets the caller know, without even reading the error message, what category of problem it is dealing with.
Python already ships a family of exceptions for the most common situations, summed up here:
| Situation | Expected type |
|---|---|
| Right type, unacceptable value | ValueError |
| A type that does not fit | TypeError |
| An operation forbidden in this state | RuntimeError |
| A feature not written yet | NotImplementedError |
| A business case specific to the project | A custom exception class |
None of these types really fits a case specific to the project's business logic, and that is perfectly fine: a class inheriting from Exception is enough to create a custom exception, catchable specifically without risking catching a language error along the way.
Raising a bare Exception, without a precise type, is tempting because it always works. The price gets paid later: nobody can catch it specifically without also catching every other one, programming mistakes included.
Re-throwing without erasing the trace
One case comes up often: an except block catches an error only to log it, without wanting to handle it itself. The question then is how to let it carry on without losing the information it was carrying.
A bare raise inside an except block answers exactly that need: it re-throws the current exception while keeping its original trace. Here is the form worth remembering:
except ConnectionError:
log("Service unreachable")
raise # the original trace is preservedRaising a new exception instead of this bare raise erases the original context, unless it is chained explicitly with raise X from error, which shows both in the traceback.
There is a deliberate variant: raise X from None. It hides the original cause, a choice sometimes made in a public library, when the internal error would tell a reader with no access to the source nothing useful.
Validate at the borders, not everywhere
Once you know how to raise exceptions, the temptation is to check the same data at every layer of the program, out of caution. The result is code weighed down with checks that repeat themselves without reliability actually improving.
The rule that works is simpler: validate once, where the data enters the program, whether that is a file, a response from a remote interface, or user input. Past that point, the rest of the code can trust what it receives. Example on reading an order:
def load_order(data):
if "quantity" not in data:
raise ValueError("Missing quantity field")
if data["quantity"] <= 0:
raise ValueError(f"Invalid quantity: {data['quantity']}")
return Order(**data)Multiplying checks deeper down adds nothing. If faulty data has already crossed half the program before being caught, the border was in the wrong place.
Frequently asked questions
How does it differ from assert?
assert checks an assumption while developing and disappears once Python runs in optimised mode: it is not a reliable validation tool. raise stays active in every circumstance, which makes it the right choice for validating data coming from outside the program.
Should the exception carry a message?
Always, and as precise as possible. A message like "Invalid value" helps nobody at debugging time, whereas "negative quantity received: -3" points straight at the offending data and saves a round trip of diagnosis.
Can an exception be raised inside an except?
Yes, and it is even common: it is the normal way to translate a technical error into a business one that the rest of the program can understand. Do chain it with from so the original cause stays visible in the trace.