Try and except
The try
and except
syntax is used to catch and safely handle errors encountered during runtime. As a new developer, you'll become accustomed to people telling you that your scripts don't work. In Python, we use the try
and except
blocks to stop preventable errors from crashing our code. Please use the try
and except
blocks in moderation. Don't use them as if they were band-aids to plug up holes in a sinking ship—reconsider your original design instead and contemplate modifying the logic to better prevent errors.
We can try
to run some line(s) of indented code and catch an exception or error with the except
statement, such as TypeError
or IndexError
, and have our program executing other logic instead of crashing. Using these correctly will enhance the stability of our program. However, improper usage will not add any stability and can mask underlying issues in our code.
When an error is encountered, an error message is displayed to the user containing debug information and the program exits. This is an undesirable yet unavoidable scenario during the life cycle of our code. Rather than wrapping our entire code with error handling, we should place it in locations where we either anticipate errors occurring or find errors during testing.
For example, say we have some code that performs a mathematical calculation on two variables. If we anticipate that a user may accidentally enter non-integer or float values, we may want to wrap a try
and except
around the calculation to catch any TypeError
exceptions that may arise. When we catch the error, we can try and convert the variables to integers with the class constructor method before trying again. If successful, we have saved our code from a preventable crash. Our catch scenario is not so vague that our program would still be able to execute if it had received a dictionary. In that case, we would want the script to crash and present debug information to the user.
It is often not advisable to cover many lines of code with one try
and except
block. Any line that has a reasonable chance of generating an error should be handled by its own try
and except
block with a solution for that specific line to ensure that we are properly handling the specific error. There are a few variations of the try
and except
block. In short, there are catch-all, catch-as-variable, and catch-specific types of block. The following pseudocode shows examples of how the blocks are formed:
# Basic try and except -- Catch-All try: # Line(s) of code except: # Line(s) of error-handling code # Catch-As-Variable try: # Line(s) of code except TypeError as e: print e.message # Line(s) of error-handling code # Catch-Specific try: # Line(s) of code except ValueError: # Line(s) of error-handling code for ValueError exceptions
The catch-all or "bare" except
will catch any error. This is often regarded as a poor coding practice as it can lead to program crashes or infinite loops. Catching an exception as a variable is useful in a variety of situations. The error message of the exception stored in e
can be printed or written to a log by calling e.message
—this can be particularly useful when an error occurs within a large multimodule program. In addition, the isinstance
built-in function can be used to determine the type of error.
In the example later, we define two functions: giveError()
and errorHandler()
. The giveError()
function tries to append 5
to the my_list
variable. This variable has not yet been instantiated and will generate a NameError
. In the except
clause, we are catching a base Exception
and storing it in the variable e
. We then pass this exception object to our errorHandler()
function, which we define later.
The errorHandler()
function takes an exception object as its input. It checks whether the error is an instance of NameError
or TypeError
, or it passes otherwise. Based on the type of exception, it will print out the exception type and error message:
>>> def giveError(): ... try: ... my_list.append(5) ... except Exception as e: ... errorHandler(e) ... >>> def errorHandler(error): ... if isinstance(error, NameError): ... print 'NameError:', error.message ... elif isinstance(error, TypeError): ... print 'TypeError:', error.message ... else: ... pass ... >>> giveError() NameError: global name 'my_list' is not defined
Finally, the catch-specific try
and except
can be used to catch individual exceptions and has targeted error-handling code for that specific error. A scenario that might require a catch-specific try
and except
block is working with an object, such as list
or dictionary
, which may or may not be instantiated at that point in the program.
In the example given later, the results
list does not exist when it is called in the function. Fortunately, we wrapped the append operation in a try
and except
to catch the NameError
exceptions. When we do catch this exception, we first instantiate the results
list as an empty list and then append the appropriate data before returning the list. Here is the example:
>>> def doubleData(data): ... for x in data: ... double_data = x*2 ... try: ... # The results list does not exist the first time we try to append to it ... results.append(double_data) ... except NameError: ... results = [] ... results.append(double_data) ... return results ... >>> my_results = doubleData(['a', 'b', 'c']) >>> print my_results ['aa', 'bb', 'cc']
Raise
As our code can generate its own exceptions during execution, we can also manually trigger an exception to occur with the built-in raise()
function. The raise()
method is often used to raise an exception to the function that called it. While this may seem unnecessary, in larger programs, this can actually be quite useful.
Imagine a function, functionB()
, which receives parsed data in the form of a packet from functionA()
. Our functionB()
does some further processing on said packet and then calls functionC()
to continue to process the packet. If functionC()
raises an exception back to functionB()
, we might design some logic to alert the user of the malformed packet instead of trying to process it and producing faulty results. The following is pseudocode representing such a scenario:
001 import module 002 003 def main(): 004 functionA(data) 005 006 def functionA(data_in): 007 try: 008 # parse data into packet 009 functionB(parsed_packet) 010 except Exception as e: 011 if isinstance(e, ErrorA): 012 # Fix this type of error 013 functionB(fixed_packet) 014 etc. 015 016 functionB(packet): 017 # Process packet and store in processed_packet variable 018 try: 019 module.functionC(processed_packet) 020 except SomeError: 021 # Error testing logic 022 if type 1 error: 023 raise ErrorA() 024 elif type 2 error: 025 raise ErrorB() 026 etc. 027 028 if __name__ == '__main__': 029 main()
In addition, raising custom or built-in exceptions can be useful when dealing with exceptions that Python doesn't recognize on its own. Let's revisit the example of the malformed packet. When the second function received the raised error, we might design some logic that tests some possible sources of error. Depending on those results, we might raise different exceptions back to the calling function, functionA()
.
When raising a built-in exception, make sure to use an exception that most closely matches the error. For example, if the error revolves around an index issue, use the IndexError
exception. When raising an exception, we should pass in a string containing a description of the error. This string should be descriptive and help the developer identify the issue, unlike the following string used. The adage "do what we say not what we do" applies here:
>>> def raiseError(): ... raise TypeError('This is a TypeError') ... >>> raiseError() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in raiseError TypeError: This is a TypeError