`raise` as a function

In Python,raise is a keyword (used to raise exceptions).

Recently I was writing some code that was using ‘boolean-operations’, like:

x = a() or b()

Resulting in x getting the value of b() if a() returned falsy.

But b() could also return falsy, and I want to detect that and raise an error. Basically either a() or b() must return non-falsy, otherwise an exception must be thrown.

I first did this by adding an extra statement:

if not x:
    raise Exception

But then I realized it would be really nice if I could just do:

x = a() or b() or raise(Exception)

Now this does not work, as raise is not a function. So I wrote a little helper function:

def raise_(e):
   raise e

And that allows me to write:

x = a() or b() or raise_(Exception)

This feels really clean to me, and makes me wonder:

Wouldn’t it be nice to have this function be added to Python?

The function could be named raise like the keyword, but could also be named differently, like throw, fail, die, etc.

1 Like

I’m not sure if it fully aligns with the purpose of the operator module, but adding a function like operator.raise_ that performs the same role as a raise statement would definitely be convenient.

1 Like

IMHO I think of raise as control flow, and I prefer control flow to be very explicit.

Personally I view the case of single-line conditioned assignment

value = a() if condition else b()

as “A value if made and assigned“, so conceptually its is more or less like

value = something

When I look at that, it is very different thing from the control flow version

if condition:
    a()
else
    b()

Which is “The function either does one thing or the other“

if condition:
    arbitrary things can happen here, including early return
else
    different arbitrary things can happen here, including early return

So for me making raise a function is kind of deceptive, because I wouldn’t expect a value = something kind of code to deliberately raise an error.

Obviously, errors can be raised anywhere (like in dictionary lookups), but my expectation is that there are two kinds of error raising a function can make:

  1. Expected errors, where a valid (or at least expected) input can lead to an error
  2. Unexpected error, where an invalid/unexpected input breaks the assumptions of the function

I feel expected errors are control flow, and therefore should be very explicit, and unexpected error should just be “ignored” (better to beg for forgiveness than ask for permission and such). A raise function to me is too implicit for the former and too explicit for the latter

# Example: I quickly scan some code and this is roughly what I imagine

def function(important, not_important=None):
    """It's a bug if important doesn't satisfy blah blah blah"""

    not_important = default  # should work if I satisfied blah blah blah

    condition = something    # should work if I satisfied blah blah blah

    if condition:
        raise Error          # here is an exit point

    return                   # here is an exit point


# Actual example code

def get_message(request, key=None):
    """Get message from request dictionary

    Expect a request dictionary with message in 'key' {key: message, ...} with an 'okay' key. default key is 'message'.

    Or not, I'm a function, not a cop.
    """

    key = "message" if key is None else key  # okay, roughly as expected

    message = request[key] or raise(ValueError, "empty message")  # Actually this is also an exit point if I have an empty message

    if not message ['okay']:   # Can raise and exit, but implicit: If I call this function with a badly-formatted message, I guess it's a bug.
        raise ValueError(message) # okay, roughly as expected

    return message # okay, roughly as expected


# What I would expect:

def get_message_expected_errors(request, key=None):
    """Get message in 'key' (default 'message') from request dictionary

    If a request is without message in 'key', or the ,essage is empty, or the messgae doesn't have 'okay' key, raise ValueError.
    If the message 'okay' is False, raise ValueError
    """
    ...
    message = request[key if key is not None else 'message']
    
    try:
        message = request[key]    
        if not message :
            raise ValueError(f"Empty message in {request=}")
        if 'okay' not in message:
            raise ValueError(f"Badly formatted {message=}")
    except KeyError:
        raise ValueError(f"Could not find message by {key!r} in {request=}")
    
    if not message['okay']:
        raise ValueError(message )
    return message 


def get_message_implicit_error(request, key=None):
    """Get message in 'key' (default 'message') from request dictionary

 
    We expect a request with a message and that message to have an 'okay' signal
    If the message 'okay' is False, raise ValueError
    """
    message = request[key if key is not None else 'message']
    if not message ['okay']:
        raise ValueError(message )
    return message 
2 Likes

If logic ‘dictates’ that a() or b() are true-ish, then or raise is an assertion. The typing module’s assert_never() fits this nicely, although you will not be able to specify a different exception type.
And of course, e.g. 1 / 0 or this_name_does_not_exist are crude alternatives for quick & dirty scripts.

An often-quoted principle is “not every 3-line function needs to be a builtin”. Yours is 2 lines, but I think the rule still applies :slightly_smiling_face: It’s easy enough to write this function when needed. And you get to choose details like what exception to raise, whereas a builtin would make the choice for you.

Plus, I doubt that the need for this is frequent enough to justify adding it to the builtins or stdlib.

You could try writing a PR to add operator.raise_, but I’d be surprised if it gets accepted, personally.

9 Likes

Thanks everybody for reading and expressing your views. I now agree with your views, and that this use-case is just too niche to break the rules for :wink:

1 Like

If you really want to one-liner it:

>>> (lambda: (yield))().throw(KeyError)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <lambda>
KeyError
1 Like

if not (x := a() or b()): raise Exception

Can do it in one line but I don’t know how I feel about it. The assignment to x persists after the conditional.

Or

(_ for _ in ()).throw(KeyError)