with match now being available, we even return explicit error objects and the ergonomics are pretty good (reminds me of rust).
with match now being available, we even return explicit error objects and the ergonomics are pretty good (reminds me of rust).
The problem I see is there's no way to enforce the check that you actually handled/bubbled up the error. With exceptions, you just crash if you don't handle it, which is much preferred if determinism is the goal. Is there some way to use the type system to do this? Maybe incoming are UnhandledResult outgoing are some HandledResult or something like that?
I don't think Python has the equivalent of C++'s [[nodiscard]] or Rust's #[must_use], though, so if there are cases where the return value can legitimately be ignored then return values risk missing errors that would show up under exceptions.
# courses.py
def create(...) -> Course | ErrInvalid:
and they get used like match courses.create(...):
case courses.Course() as course:
# We use `course` below.
pass
case courses.ErrInvalid() as err:
# Error conditions are usually translated into early returns
raise fastapi.HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY, err.message
)
return APICourseCreateResponse(id=course.id, ...)
It has worked (for us at least) pretty well to reduce the number of unhandled errors that make their way into Sentry. The granularity of the error types depends on the need (e.g., sometimes we just have an `ErrInvalid` that returns a message which gets displayed to the user, but sometimes we'll have ErrInvalidAddress or ErrInvalidTaxCode to be more specific for the context).And yeah, without nodiscard, we try™ to only do this on functions that return values that the caller will definitely need to use. Also combined with exhaustiveness checks on switch statements by default.