Lesson 1 - Exceptions in Python
In this Python course, we'll focus on working with files. However, before we can start writing and reading data files, we should figure out how to handle program error states, which will occur a lot when working with files.
An error can often occur in our program. I don't mean an error due to the fact that the program was poorly written, we are able to avoid such errors well. In general, these are mainly errors that are caused by the input/output operations. We can abbreviate "input/output" as IO. This is, for example, user input from the console, a file, output to a file, printer etc. In principle, there's the user who can give us invalid input, a non-existent or invalid file, disconnect the printer, and so on. However, we'll not let the program crash with an error, on the contrary, we'll treat vulnerabilities in the program and alert the user to the given fact.
Active Error Handling
The first error handling option is called "active". In the program, we map
all vulnerabilities and treat them with conditions. Zero division is usually
used as a textbook example. Imagine a program that uses a Math
class that has a quotient()
method. The class might look like
this:
class Math(): def quotient(self, a, b): return a / b
We'll now use the class as follows:
print("Enter a divident and divisor to calculate the quotient:") a = int(input()) b = int(input()) print(Math().quotient(a, b))
If the user enters the numbers 12
and 0
in the
program, the program will crash with an error because it cannot divide by zero.
We actively treat the error with a simple condition in the program:
print("Enter a dividend and divisor to calculate the quotient:") a = int(input()) b = int(input()) if b != 0: print(Math().quotient(a, b)) else: print("Cannot divide by zero.")
Now we have to make sure that we don't input a zero in the second parameter every time we use the method. Imagine the method processed 10 parameters and we used it several times in the program. It'd certainly be very difficult to sanitize all uses of this method.
The solution could be to insert the check directly into the method. However,
we have a new problem here: What value do we return when the 2nd parameter is
zero? We need a value from which we know that the calculation didn't work
correctly. This is a problem when we choose, for example, zero, we don't know
whether, for example, 0/12 is an incorrect calculation or not. We can't tell
whether 0
indicates a result or an error. We can't do it using
negative numbers either. Parsing values is the 2nd classic example of vulnerable
user input. Other are file operations, where the file might not exist, we might
not have rights to it, it might be used with other program and so on.
Passive Error Handling
Especially when the operation is more complex and it'd be too difficult to
treat all possible error conditions, exceptions come to play,
the passive error handling. We don't need to be interested in the
internal logic in the method we call. We'll try to run the dangerous part of the
code in "protected mode". This mode is slightly slower and differs in
that if an error occurs, we have the option to catch it and prevent the program
from crashing. We speak of the error here as of an exception.
We use the try
- except
blocks:
try: pass except: pass
We place the dangerous part of the code in the
try
block. If an error occurs in the try
block, its
execution is interrupted and the program goes to the except
block.
If everything goes well, the try
block is executed fully and the
except
block is skipped. Let's try the situation in our previous
example:
try: print(Math().quotient(a, b)) except: print("Error during division.")
The code is simpler in that we don't have to treat all the vulnerabilities
and think about what could go wrong. We only wrap the risky code with a
try
block and all errors are caught in except
. Of
course, we only place what is absolutely necessary in the try
-
except
block, not the whole program
So now we know how to handle situations where the user enters an input that could cause an error. It doesn't have to be just file operations, exceptions have a very wide range of uses. We can write our program in such a way that it can't be easily crashed by the user.
Using Exceptions When Working With Files
As already mentioned, file operations can throw many exceptions, so
we always work with files in a try
- except
block. There are also several other constructs that we can use with
exceptions.
Finally
We can add a 3rd block to the try
- except
called
finally
. It always runs whether or not an
exception has occurred. Imagine the following method to save
settings:
def saveSettings(): try: f = open("file.dat", "w") f.write("Some settings...") f.close() # we need to close the file handler except: print("Error writing the file.")
This method tries to open the file and write some settings to it. We must
then close the open file. A message is printed to the console in case of an
error. However, listing errors directly in a method is ugly, we already know
that methods and objects in general should only perform logic, and communication
with the user is done by the function which calls them. So let's return
True
/ False
depending on whether the operation was
successful or not:
def saveSettings(): try: f = open("file.dat", "w") f.write("Some settings...") return True except: return False f.close()
At first glance, it looks like the file always closes. However, the whole
code is in a method in which we call return
. As we know,
return
terminates the method and nothing after it is executed.
Here, the file would always remain open and closing would no longer take
place. As a result, the file could then be inaccessible. If we close the
file in the block finally
, it'll always be executed.
Python remembers that the try
- except
block contained
finally
and calls the finally
block even after leaving
the except
or try
block:
def saveSettings(): try: f = open("file.dat", "w") f.write("Some settings...") return True except: return False finally: f.close()
It'd be best to leave out the except
block altogether and let
the method throw the exception. We'll rely on whatever called the method that
threw the exception to deal with it, instead of the method itself. That's
better, since we save the return value of the method (which can then be used for
something else) and the code is simplified for us:
def saveSettings(): try: f = open("file.dat", "w") f.write("Some settings...") finally: f.close() }
The file always closes in the code above, even if something goes wrong during writing, e.g. the storage space runs out. We'd now call the method like this:
try: saveSettings() except: print("Failed to save settings.")
We'll now show you how to simplify the whole situation even more. We use the
with
construct.
The with
Construct
Python greatly simplifies working with instances of classes to read and write
to files or, in general, classes that need to perform any cleanup work. The
above block can be written using the with
notation, which replaces the try
and
finally
blocks. A huge advantage is that Python generates
the finally
block itself and ensures that the instance closes the
file. Using with
, the saveSettings()
method would look
like this:
def saveSettings(): with open("file.dat", "w") as f: f.write(object) }
We can see that the code has become extremely simple, even though it does
essentially the same thing. When calling the method, we'll use the
try
- except
block again.
Remember that with
only replaces
try
- finally
, not except
!. The
method in which with
is used must still be called in the try-except
block.
Now we've reached exactly where we needed to. In all the following tutorials,
we'll use the with
construct for all file manipulations. The code
will be simpler and we'll never forget to close the file.
We'll return to exceptions in the future to show you how to catch only some of the types of exceptions, ready-made exception classes we can use in our programs, and how to create our own exception. Now, however, I just wanted to explain the necessary minimum for working with files while not confusing you with complex constructions.:)
In the next lesson, Introduction to Working With Files in Python, we'll look at how to work with file write rights in Windows and try the first few file operations.