Unexceptional exceptions are Exception objects that are returned as "normal" error objects, with return type hints, for "normal" non-try-except execution flow in some, but not necessarily all, layers of a Python application.
With return type hints, type checkers, such as Mypy, can help spot when unexceptional exceptions are not handled properly.
For a nice discussion on returning error objects versus raising exceptions, read Raising exceptions or returning error objects in Python by Luke Plant.
The short answer is no. A Mypy maintainer has stated, "mypy is extremely unlikely to do this". For more details, see:
Nevertheless, by returning unexceptional Exception types, you can leverage Mypy to help spot when unexceptional Exception types are not handled properly.
A do_ function pattern can ease the introduction of unexceptional exceptions.
When first writing a function, the natural and easy approach is to be Pythonic and not return exceptions. It can be useful to initially not worry about what errors or Exception types should be part of a function's typed interface. This often does not become clear until after implementation and some usage of the function.
If later it makes sense to have a typed function interface with returned error objects,
then a new do_ function can complement the initial Pythonic exception-raising function.
Consider an initial Pythonic function with the declaration:
def fancy_math(x: float) -> float:Later, it becomes clear that ValueError is an unexceptional error case.
A do_ function has the same parameters, the same name (but with do_ prefixed),
and the same return type, but with all unexceptional exceptions appended to the
return type:
def do_fancy_math(x: float) -> float | ValueError:The implementation of fancy_math is ported to do_fancy_math
to return, rather than raise, unexceptional exceptions.
By using the unexceptional helper function, a stack trace is included,
even though the Exception object is being returned rather than raised.
The new implementation of fancy_math is a simple one-liner using the
cast_or_raise helper function.
def fancy_math(x: float) -> float:
return cast_or_raise(do_fancy_math(x))Callers have a choice. They can be Pythonic and continue to call fancy_math with a
possible ValueError being raised, and may try:.
Or the caller can choose to not try: and instead call the do_ function:
y = do_fancy_math(x)
if isinstance(y, ValueError):
...... so as to use the Force:
"Do. Or do not. There is no try." -- Yoda