In a previous post, I presented a C#
Result class that represents the outcome of an operation. This class is intended to be used for error handling as an alternative to throwing and handling exceptions. I was introduced to this concept by a post from the Enterprise Craftsmanship blog. I recommend reading the entire post, which is part of a series examining how principles from Functional Programming can be applied to C#.
I thought it would be interesting to implement the
Result class in Python, and since Python is dynamically-typed this ended up being much simpler than the C# implementation which required the use of generic types. The entire implementation is given below:
Result class encapsulates all information relevant to the outcome of an operation. For example, let’s say that we have a
Result instance named
result. If the operation which
result represents failed,
result.success will be
result.error will contain a string detailing why the operation failed. If the operation succeeded,
result.success will be
result.error will be
None). If the operation produced any output, this will be stored in
result.value (an operation is not required to produce an output). Result objects are not intended to replace exception handling in all scenarios, and the author of the EC blog provides a simple rule to determine when each should be used:
- Use a
Resultobject for expected failures that you know how to handle.
- Throw an exception when an unexpected error occurs.
To demonstrate how the
Result class should be used, the function
decode_auth_token in module
app.util.auth validates an access token in JWT format. Please note the highlighted line numbers:
- Lines 11-13: If you call a function that returns a
Resultobject, you should check the value of
result.success). I prefer checking
result.failureto reduce unnecessary indentation.
- If the operation failed, you should handle the failure immediately or return the result object upstream until you reach an appropriate place to handle and/or report the failure.
- If the operation was successful and you expect the function to return a value, you can retrieve it by calling
result.value. If no value is expected, (as is the case for the
check_blacklistfunction) you simply keep executing your current function.
- Lines 16 and 29: To indicate that a function (operation) was successful, the function should return
Result.Ok(). You may have noticed in the
Resultclass that providing a
valueas a parameter is optional. If the successful operation produces a result (e.g.
payload['sub']) the client can retrieve it from
- Lines 19, 22, 28 In the case of decoding a json web token, we expect exceptions
jwt.InvalidTokenErrorto occur and we know how to handle them (Deny the user from performing the requested action and prompt them to re-authenticate). This is the exact use case we defined for the
Resultclass earlier in this post. To indicate that a function has failed, return
errorshould be a message explaining why the operation failed).
The Python REPL code below demonstrates how the
decode_auth_token function behaves and how to interact with the
Result objects that the function returns:
>>> access_token = request.headers.get('Authorization') >>> result = decode_auth_token(access_token) >>> result Result<success=True> >>> result.success True >>> result.value '570eb73b-b4b4-4c86-b35d-390b47d99bf6' >>> result.failure False >>> result.error >>> print(result) [Success] >>> exit()
>>> auth_token_bad = request.headers.get('Authorization') >>> result = decode_auth_token(auth_token_bad) >>> result Result<success=False, message="Invalid token. Please log in again."> >>> result.success False >>> result.value >>> result.failure True >>> result.error 'Invalid token. Please log in again.' >>> print(result) [Failure] "Invalid token. Please log in again." >>> exit()
>>> auth_token_expired = request.headers.get('Authorization') >>> result = decode_auth_token(auth_token_expired) >>> result Result<success=False, message="Access token expired. Please log in again."> >>> result.success False >>> result.value >>> result.failure True >>> result.error 'Access token expired. Please log in again.' >>> print(result) [Failure] "Access token expired. Please log in again." >>> exit()
I find that code becomes easier to read and digest visually when the
Result class is incorporated. It becomes easier to discern what happens when a failure occurs and how the failure is handled, in contrast to a design that favors exception handling as the primary method of error handling.
I have taken the time to explain the Python version of the
Result class because it will be referenced frequently in upcoming posts. As always, please give me your feedback or questions in the comments!