Hi everyone, I've read https://stackoverflow.com/questions/44282268/python-type-hinting-with-except... and especially the https://jonathan-scholbach.github.io/2023/02/06/what-does-it-mean-to-be-exce... blog post, but I want to keep coding with exceptions rather than returning `Ok | Err`. Here is a simple pattern I'm thinking to use, to let a function indicate the `Exception`s it may raised, which can be used: - by the developper as documentation, - by the static type checker for exhaustiveness ```python from typing import NoReturn, cast, TypeAlias class SomeError(Exception): ... class AnotherError(Exception): ... class StillAnotherError(Exception): ... FooError: TypeAlias = ValueError | SomeError | AnotherError def assert_never(value: NoReturn) -> NoReturn: # This also works at runtime as well assert False, f"This code should never be reached, got: {value}" def foo(i: int): if i == 0: raise ValueError("Must not be zero") elif i == 1: raise SomeError("Must not be one") elif i == 2: raise AnotherError("Must not be two") print("that's ok") try: foo(0) except Exception as e: e = cast(FooError, e) if isinstance(e, ValueError): print("value error") elif isinstance(e, SomeError): print("some error") else: assert_never(e) raise ``` It's far from being perfect, but it's simple and not too much intrusive nor obscure. In the above example, `mypy` can tell me which exception I forgot to manage: ``` foo.py:43: error: Argument 1 to "assert_never" has incompatible type "AnotherError"; expected "NoReturn" [arg-type] Found 1 error in 1 file (checked 1 source file) ``` What do you thing of this? Here is a proposal where the exception are annotated in the return type, and the try/except would directly check for exhaustiveness: ``` from typing import NoReturn, Annotated class SomeError(Exception): ... class AnotherError(Exception): ... class StillAnotherError(Exception): ... def assert_never(value: NoReturn) -> NoReturn: # This also works at runtime as well assert False, f"This code should never be reached, got: {value}" def Raises(exceptions): ... # TODO class NeverException(Exception): ... # TODO def foo(i: int) -> Annotated[None, Raises(ValueError | SomeError | AnotherError)]: if i == 0: ¦ raise ValueError("Must not be zero") elif i == 1: ¦ raise SomeError("Must not be one") elif i == 2: ¦ raise AnotherError("Must not be two") print("that's ok") try: foo(0) except ValueError: print("value error") except SomeError: print("some error") except NeverException: # static type checker warns here that AnotherError is not managed raise ``` and here is another fictive example with one intermediary function managing one of the 3 exceptions: ```python from typing import NoReturn, Annotated class SomeError(Exception): ... class AnotherError(Exception): ... class StillAnotherError(Exception): ... def assert_never(value: NoReturn) -> NoReturn: # This also works at runtime as well assert False, f"This code should never be reached, got: {value}" def Raises(exceptions): ... # TODO class NeverException(Exception): ... # TODO def foo(i: int) -> Annotated[None, Raises(ValueError | SomeError | AnotherError)]: if i == 0: ¦ raise ValueError("Must not be zero") elif i == 1: ¦ raise SomeError("Must not be one") elif i == 2: ¦ raise AnotherError("Must not be two") print("that's ok") def bar(i: int) -> Annotated[None, Raises(ValueError | SomeError)]: try: ¦ foo(i) except AnotherError: ¦ print("another error") try: foo(0) except ValueError: print("value error") except SomeError: print("some error") except NeverException: # no errer reported by the static type checker raise ``` Thanks!