I have to admit, this is one of the most challenging topics which I have approached, until now… 🙂 So… I decided to split it into two postings.
In the first one, we will have a look over:
- try statement,
- exceptions for programmer errors,
- exceptions, APIs and protocols,
- dealing with failures.
I aim to understand this, both from theoretical and practical side by looking at some examples.
Let the story begins…
Exception handling is a mechanism for stopping “normal” program flow and continuing at some surrounding context or code block.
Raise an exception to interrupt program flow. Handle an exception to resume control.
If an exception propagates up the call stack to the start of program, then an unhandled exception will cause the program to terminate.
Exception objects contain information about where and why an exceptional event occurred. Exception object is transported from the point which the exception was raised to the exception handler, so that the handler can interrogate the exception object and take appropriate action.
There have been long and tiresome debates over exactly what constitutes an exceptional event, the core issue being that exceptionality is in reality a matter of degree, some things are more exceptional than others. Whereas programming languages impose a false dichotomy by insisting that an event is entirely exceptional or not at all exceptional. The Python philosophy is at the liberal end of the spectrum when it comes to the use exceptions. Exceptions are ubiquitous in Python and it is crucial to understand how to handle them.
The try statement
For a better understanding on how to handle selected exceptions, look at the following example:
In the code above we try to make our convert() more robust by handling both ValueError and TypeError using a try except construct. Both the try and except keyword introduce new blocks. The try block contains the code that can rise an exception, and the except block contains the code which performs error handling in the event that exception is raised.
Exceptions for programmer errors
Almost anything that goes wrong with the Python program results in an exception, but some such as IndentationError, SyntaxError, NameError are the result of programmer errors which should be identified and corrected during development rather than handled at runtime. Normally, we should not catch these error types.
Exceptions, APIs and Protocols
API – application programming interface
Exceptions are parts of families of related functions and they are part of certain protocols. For example, objects which implement the sequence protocol should raise an IndexError exception for indexes which are out of range. The exception which are raised are as much a part of a function’s specification as the arguments it accepts, and as such must be implemented and documented appropriately.
There are a handful of common exception types in Python, and usually when you need to raise an exception in your own code, one of the build-in types is a good choice. Much more rarely you’ll need to define new exception types. Often, if you are deciding what exception your code should raise, you should look for similar cases in existing code. The more your code follows existing patterns, the easier it will be for people to integrate and understand. Therefore, a good practice would be to use common or existing exception types when possible and try to follow existing usage patterns.
Let’s have a look at a few common exception types:
- IndexError – integer index is out of range.
- KeyError – look-up in a mapping fails.
- ValueError – object is of the right type, but contains an appropriate value.
- TypeError – even if in the previous example we handled the TypeError, in practice it is recommended to avoid protecting against TypeErrors. This is generally “against the grain” in Python.
It’s usually not worth checking types. This can limit our functions unnecessarily.
Dealing with failures
There are two approaches to dealing with a program operation that might fail:
- First approach is to check that all the preconditions of a failure-prone operation are met in advance of attempting the operation.
- The second approach is to blindly hope for the best, but be prepared to deal with the consequences if it doesn’t work out.
Therefore, Python culture has two philosophies:
- LBYL – Look Before You Leap
- EAFP – It’s Easier to Ask Forgiveness than Permission
Python is strongly in favor of EAFP because it puts primary logic for the happy path in its most readable form, with deviation from the normal flow handled separately, rather than interspersed with the main flow.
Let’s take as example the processing of a file. The details of the processing are not relevant. All we need to know is that process_file() function will open a file and read some data from it.
The LBYL version. In the code below, before attempting to call process_file, we check that the file exists and if it does not, we avoid making the call, and print a helpful message instead. The details of the processing are not relevant. The only thing we need to know is that the process_file() function will open a file and read some data from it.
There are several problems with this approach. One of these problems is that we only check for existence. What if the file exists but contains garbage? What if the path refers to a directory instead of a file? According to LBYL, we should add preemptive tests for these too.
A more subtle problem is that there is a race condition here. It is possible for the file to be deleted, for example by another process, between the existence check and the process_file() call, a classic atomicity issue.
There is really no good way to deal with this. Handling of errors from process_file() will be needed in any case.
The EAFP approach.
Here we simply attempt the operation without checks in advance, but we have an exception handler in place to deal with any problems. We do not even need to know in a lot of detail exactly what might wrong. Here we catch OSError. Which covers all manner of conditions such as file not found and using directories where files are expected.
To be continued…