On Sat, Sep 26, 2020 at 09:59:27AM +0200, Sascha Schlemmer via Python-ideas wrote:
I think a clear distinction has to be made between errors that belong to the api surface of a function e.g. some `ValueError` or `ZeroDivisionError` and *exceptional* errors e,g.`KeyboardInterrupt` or `DatabaseExplodedError`.
One problem is that while we can make a distinction between "expected" errors and "unexpected" errors, it's subjective and anything but clear. Whether you handle it, or just let it go, depends in part on the software you are writing.
For the first case I think a better way to model a function, that has some *expected* failure mode is to try and write a *total* function that doesn’t use exceptions at all. Let me show what I mean:
``` def div(a: int, b: int) -> float: return a / b ```
This function tries to divide two integers, trouble is that `div` is not defined for every input we may pass it, i.e. passing `b=0` will lead to `ZeroDivisionError` being raised. In other words `div` is a *partial* function because it is not defined for every member of its domain (type of its arguments).
There are two ways to amend this issue and make `div` a *total* function that is honest about its domain and co-domain (types of input arguments and return type):
You're making a mighty effort to bring some functional programming concepts to Python, but it doesn't really work. For example:
1. Extending the type of the functions co-domain (return type) and making the error case explicit [...]
``` def div(a: int, b: int) -> Maybe[float]: try: return Some(a / b) except ZeroDivisionError: return Nothing() ```
Alas, this still doesn't work because type annotations are advisory and not mandatory. Regardless of the type hints given, I can still call `div(1, "surprise")` and get a TypeError. Worse, because types in Python use subtyping, I can pass an int that does something you don't expect: class MyWeirdInt(int): def __truediv__(self, other): if other == 0: import loging logging.logg("tried to divide by " + other) raise ValueError('divided by zero') ... I've intentionally included at least three bugs in that method. Obviously in practice only one is likely to occur at a time, but this illustrates that even if the arguments to your div function are ints, in this case a subclass of int, you might expect a ZeroDivisionError but actually get: * ImportError (`import loging`) * AttributeError (`logging.logg`) * TypeError (concatenate a string with a number) * ValueError (explicit raise) or any other exception at all. So your "total function" is still not total: as soon as we move beyond the builtins written in C, literally anything can happen and any promise you make in `div` can be only advisory, not guaranteed. To get your "total function" you have to catch *anything*, but that cure is worse than the disease. That means that obvious bugs get suppressed and turned into `Nothing()` when they should in fact raise. And this brings us to where Java discovered that checked exceptions are counter-productive. To satisfy the compiler and have a "total function", anything which uses CheckedExceptions eventually ends up having the Java equivalent of: # Declared to only raise SomeError try: actual implementation except: # Catch anything and everything. raise SomeError("something bad happened") because that's the only way to end up with a total function in your sense, given that subclasses can raise anything. And that's why many of us are quite resistant to attempts to bring checked exceptions to Python. -- Steve