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

2 Likes

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

10 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
3 Likes

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)
3 Likes

Just to be clear (I agree with the reasons given for not doing this), the problem isn’t that raise isn’t a function, it’s that it is a statement, rather than an expression, so it can’t be part of a larger expression (compare to assignment, were x = a() or b() or c = 3 is illegal, but x = a() or b() or (c := 3) is legal, because := is an expression, = is a statement). You could keep the raise keyword just fine and tweak it to be a legal expression to get what you want, without requiring a function.

Yes, making raise an expression works like how yield is really an expression but in most use cases it’s written more like a statement.

Despite the technical feasibility I don’t see this idea getting accepted unless a raise expression can do more than just saving a line of code though.

1 Like

As commented already months ago, raise changes the flow control. I’d like to re-phrase that: raise would become an expression without value and thus unusable in containers. I cannot imagine benefits large enough to outweight this being legal:

little_horror = [10, 20, raise(ValueError), 40]
1 Like

A hypothetical raise expression is meant to be used with other flow control operators, namely and, or and if-else (and possibly lambda). It doesn’t have to be used in any other places where it makes no sense.

But you can already do this today–just use a raise_ helper function, and it would make as much or as little sense as a raise expression if you insist on using it in a container for no good reason.

1 Like

Yes, that’s fair and technically you are right.

Mentally I see a difference between those two. This is not an expression created by combining several values and operators in a way that cannot be correctly evaluated. I understand preparing such thing as a “barrier”. Perhaps tiny (as in 1/0), but existing, so it can be noticed it in one’s mind.

Hypothetic raise would be an “atomic” expression that always raises. (side note: it probably wouldn’t be called an expression in mathematics). My dislike is based on the fact, that there would be no “barrier”. I hope I could explain my view with my current level of English language.

Not that I would be in favor of a raise expression, but it is a nice thought experiment in order to learn more about Python’s syntax. Let’s say, we allow raise as expression and someone writes

x = raise RuntimeError if type(s) not in (str, bytes) else len(s)

The idea would be to obtain the length of s if it was a string or a bytes object, but throw RuntimeError otherwise. One would expect that the expression on the right side of the assignment could be used as an expression statement:

raise RuntimeError if type(s) not in (str, bytes) else len(s)

This code is (syntactically) valid Python code today. According to the grammar, raise has the following form

raise_stmt:
    | 'raise' expression ['from' expression ] 
    | 'raise' 

So, it is followed by an expression, like RuntimeError if type(s) not in (str, bytes) else len(s) in the code above. So, this will either raise a RuntimeError or a TypeError (which is created when one tries to throw an int). My assumption then would be that the x=...version would do the same.

So, the version above is not feasible for backwards-compatibility reasons. Parenthesis to make it look like a function would not change anything:

raise(RuntimeError) if type(s) not in (str, bytes) else len(s)

is exactly the same code as above. The only way to spell an raise expression in this context would actually be

(raise RuntimeError) if type(s) not in (str, bytes) else len(s)

That would actually be possible (also not a proposal), but it would also open a whole new can of worms (which some people might find quite tasty): statement-expressions, this is, the possibility to use statements in a context that otherwise only allows expressions by enclosing the statement in parenthesis.

4 Likes

that one. Just what the OP needs!

I think this spelling of “obtain the length of s if it was a string or a bytes object, but throw RuntimeError otherwise” more clear:

x = len(s) if type(s) in (str, bytes) else raise RuntimeError

REALLY not a fan of this, and I would rewrite that as a statement. If for some reason it actually makes sense to do this as an expression, a helper function for raising would be fine.

1 Like