raise in Python: triggering an error on purpose

The raise statement triggers an exception on purpose, to report a situation the code must not handle in silence.
5 min read
Believemy logo

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:

PYTHON
def divide(a, b):
    if b == 0:
        # Stop right here, not three lines further down
        raise ValueError("The divisor cannot be zero")
    return a / b


Why 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:

PYTHON
# 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:

SituationExpected type
Right type, unacceptable valueValueError
A type that does not fitTypeError
An operation forbidden in this stateRuntimeError
A feature not written yetNotImplementedError
A business case specific to the projectA 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.

Warning

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:

PYTHON
except ConnectionError:
    log("Service unreachable")
    raise                     # the original trace is preserved

Raising 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.

Good to know

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:

PYTHON
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

Question

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.

Question

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.

Question

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.

Related terms

Discover our python glossary

Browse the terms and definitions most commonly used in development with Python.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.