Allow for the Hinting of Raised Exceptions Within a Callable

I would love to see Python add the ability to hint exceptions that can be raised within a function/callable. Other languages such as Java have this feature. Something like this for example:

def myfunc(x, y) -> [int, (OSError, RuntimeError)]:
    …

Or like this

def myfunc(x, y) raises (OSError, RuntimeError) -> int:
    …

Or like this

def myfunc(x, y) -> int:
    raises OSError, RuntimeError
    …

Im not too particular on the actual syntax. This would great for a few reasons:

  1. IDE autocomplete could suggest exceptions in except clauses automatically based on the types raised from the operations within the try block. They could also “gray-out” except clauses that will never be triggered by their corresponding try block.

  2. Much more clarity on where exceptions actually come from. This could further enable tools that support automatic documentation.

  3. Linters can provide warnings/suggestions when try/except blocks don’t cover the full range of possible exceptions, warn when operations could be moved outside the try block since none of the except clauses actually cover the operation, or warn when attempting to catch an exception that isn’t thrown.

Search these forums for “checked exceptions”. They are a terrible idea.

4 Likes

While I agree with some of the criticisms on previous topics for this idea, I disagree with some of them as well.

Namely the biggest complaint I saw is “there’s no way to guarantee x, y, and z are the only exceptions that will be raised”. What I’m proposing is a function simply being explicit about what it KNOWS will be raised.

In the example above, OSError and RuntimeError are known to be raised. It’s not a declaration that “these are the only exceptions that can be raised” but rather “these can definitely be raised by the function, but it’s possible an undocumented exception could be raised as well”.

Why I think it’s an improvement to the language is that 90% of the try/except blocks in Python code I see look like this

try:
    something()
except BaseException:
    print(“some error occurred”)
    …

This is because with a lot of libraries (especially libraries with bindings to Python written in another language) it’s very nontrivial or even impossible to find out what exception is actually raised for the problem you are trying to handle. This behavior is problematic in my opinion.

As long as the system remains opt-in I don’t see why it would create issues, but then again maybe there’s something I’m missing.

Okay. What are the exceptions that this function can raise?

def remainder(x, y):
    if y == 0: raise ZeroDivisionError
    return x % y

Here, I’ll start the list for you:

  • ZeroDivisionError, obviously
  • OverflowError - try remainder(1<<1000000, 2)
  • KeyboardInterrupt
  • MemoryError
  • TypeError, many different ways

Checked exceptions have to also include any exceptions that could come from what your function calls, otherwise they’re basically useless (you could just see by looking at the source code of the function to see if there are any raise statements). But that also makes them almost completely useless.

That’s bad code. Terrible, horrible code. (Assuming the omitted portion isn’t logging the exception or something, that is.) Checked exceptions will NOT solve this problem, they’ll just turn it into something equivalently bad, but which is now tightly bound to the function it’s calling.

Assume that any code can raise any exception. Now flip your question around: don’t concern yourself with what might be raised, concern yourself with what you can actually handle.

Checked exceptions are still terrible. Nothing has changed that in the past several decades.

2 Likes

Hmm I suppose what you’re saying makes sense. I just think the whole “assume code can raise any exception and handle what you can” would cause some super overly verbose error handling such as

def do_something(x, y):
    …

try:
    something = do_something(3,4)
except FileNotFoundError:
    # I guess this “could” happen
except ImportError:
    # I guess this “could” happen
except asyncio.TimeoutError:
    # not an async function but I guess this “could” happen
except json.JsonDecoderError:
   # I don’t do anything with Json but the things I call in do_something may so I guess this “could” happen
… (100 more lines of except clauses)

Now obviously this is exaggerated, but the point that error handling is overly verbose when you aren’t aware of what to look for still stands. You may end up handing many exceptions that realistically will never happen simply out fear that the “could” happen, or because the particular error you’re trying to handle could result in any arbitrary exception

All of this because “I want to handle when thing x goes wrong, but I have no idea what exception is actually raised when thing x goes wrong, so I guess I’ll handle as many as I can unnecessarily hoping one of these except clauses does the trick”

Yes, and it’s not just verbose, but worse than useless. What’s in all those except blocks? Are you simply reraising the exception? That’s the least bad option, but (a) that’s what exceptions do by themselves, and (b) that kinda demonstrates how little value the checked exceptions are giving you. Are you logging the error and swallowing it? Now you can’t handle that error at any other level in the call stack, which otherwise is one of the strengths of exception handling. Are you swallowing the error and NOT logging it (or printing a generic message)? Then you’ve made debugging harder for the future.

There is basically no value in this sort of “except clause spam”.

Brought up last week: What if function definitions could tell the caller "I raise Exceptions"?
And last month: Extend type hints to cover exceptions
as well as numerous times before that over the years.

The answer is pretty definitively “not going to happen” at this point.

1 Like

Here is the working syntax:

It is compatible with all maintained Python versions. All you need is to teach your IDE to recognize and use it. But you would need to do this in the case of any other syntax.

1 Like

Google style docstrings have a convention for how to document exceptions and this style is already supported by some editors and linting tools:

def foo()
    """...

    Raises:
        IOError: An error occurred accessing the smalltable.
    """
5 Likes

I’m not bashing the OP here, maybe it’s just a lack of searching before making a post but does not one else find it odd at the number of created posts on this topic in the past 6 months? Is this like number 4? Maybe I’m misremembering or the fact that some of the previous ones got so large Im conflating it with multiple posts…

With that being said, is this trolling at this point? I feel as if this subject is being discussed ad nauseum

No, I don’t think it’s trolling. As far as I can tell, everyone who’s raised this (pun intended) has done so in good faith, and often from a slightly different starting point, which makes the classic “search the forum before you post” rule less effective. It’s just that checked exceptions are a very obvious solution to a problem that only exists because of a flawed understanding of exception handling.

2 Likes