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`. For the latter case exceptions work perfectly well as they are and it is both unhelpful and infeasible to annotate/document every exception - just let the exception bubble up and someone will handle it (might even be a process supervisor like supervisord) 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): 1. Extending the type of the functions co-domain (return type) and making the error case explicit (see https://returns.readthedocs.io/en/latest/index.html https://returns.readthedocs.io/en/latest/index.html for a library that implements this style of error handling) ``` def div(a: int, b: int) -> Maybe[float]: try: return Some(a / b) except ZeroDivisionError: return Nothing() ``` or ``` def div(a: int, b: int) -> Result[float, ZeroDivisionError]: try: return Success(a / b) except ZeroDivisionError as error: return Failure(error) ``` Now `div` does return a valid instance of `Maybe` (or `Result` if more a more detailed failure case is wanted) which is either something like `Some(3.1415) ` or `Nothing` (analogous to `None` ). The caller of the function then has to deal with this and mypy will correctly warn if the user fails to do so properly e.g. ´´´ div(1,1) + 1 # mypy will give a type error ´´´ 2. Restricting the functions domain to values with defined behavior ``` def div(a: int, b: NonZeroInteger) -> float: return a / b ``` In this case the burden is put on the caller to supply valid inputs to the function and this effectively pushes the error handling/ input validation out of `div` A language that does all this really well is F# (which like python is a multi-paradigm language that offers both object-oriented and functional-programming-oriented constructs). The trouble in python at the moment is that using something like `Maybe` and `Result` is not as nice to use as exceptions due to lack of a nice syntax i.e. for exceptions we have `try`/`except` and for functional error handling we’d need pattern matching.