2017-11-08: the section on double assignment was added.
Errors are typically indicated by a non-zero integer value. Within a program/library, which is the main focus here, error return values are negative. For a program, error exit values are positive. In both cases, a zero value indicates success.
Integers do not carry much information. And what information they do carry often needs to be looked up in a table (e.g., what is
errno 11?). On the other hand, a (good) string is explanatory and can be tailored to specific situations. This is why error messages are often output to stderr, even when a non-zero exit code is returned. But error strings are not just useful for end users. Even with libraries, error strings can be useful for logging. But this is more than a discussion of error strings or not.
One of the alternatives to integer-based errors in Python is to use exceptions. This can be useful, but still, it is an exception rather than just an "error". The exception vs error debate is not addressed here, but see LBYL and EAFP for more on Python's philosophy re: errors and exceptions.
The point of this article is to present an
Error class in the spirit of Go error handling and consider its use/application in Python from a personal perspective.
I have used the following in a number of projects:
Error allows me to:
- indicate an error situation
- provide an arbitrary, but useful, text description of the error
- pass it around as an error object rather than an undifferentiated integer
The first case is when the callable acts like a subroutine (no value is returned explicitly; in which case Python returns
This is easy to use because a
None return value will always be treated as success, as anything else will be an
The second is when the callable acts like a function (a value is returned):
This situation requires a bit more effort to use, but Python makes it easy because
val can be any value, including
A third use case is really just a different flavor of the above:
This one is very familiar to Go programmers and avoids the overloading of the single return value case. The main difference is that Go provides a more efficient way to write this:
Combining all of the above in the form of "double assignment":
There are some clear benefits of this approach:
- the code is succinct,
- that the function/method returns a non-error or an error result is documented by the double assignment,
- the result (error or not) can be accessed using a suitably named variable, as the code warrants.
The only potential drawback is the cost of the double assignment. But given the use cases of Python, this likely has minimal overall performance impact while the readability and understandability of the code increases.
Of course, the
Error class can be extended to provide a collection of classes to more precisely indicate the type of error. E.g.,
These can then be tested for using
isinstance() to match
Error or the specific subclasses.
It is also possible for a subclass of
Error to be augmented with other information such as an exit code (!), stack information, other context-specific information. E.g.,
But the core ideas are that errors 1) are more than just integers, and 2) have a useful string representation.
Some Pros and Cons
One of the main drawbacks is that
Error is unique to my package. It would be nice if there was a canonical
Error (as there is an
Exception). However, it may not be as much of a problem as it first appears. If errors from one layer need to be passed to another, a conversion between different, unrelated
Error implementations can be done trivially:
After all, the programmer should be aware of interfaces/interactions between layers and take care of checking and returning appropriate values. And, as presented,
Error always provides a simple text string.
A perceived drawback is that this approach encourages a lot of error testing, a practice which Go programmers typically defend as being necessary for code to be good: errors should be checked.
The primary benefit of
Error over a simple error code is that useful, contextual information can percolate up from anywhere in the code and, if appropriate, be used to inform the user. Too often, a helpful error code is converted to a generic error code and returned, thus losing information. Code-wise, the
Error type is useful; people-wise, the text is useful.
Even in Python, exceptions are not always the best way to do things. Sometimes returning an error code or similar is appropriate. In such situations, wherever one lands on the question of using something like
Error instead of integer return values for errors, the approach is worth considering.
Error is not always appropriate, but it does make one think about the merits of how and why things are done as they are.