SoFunction
Updated on 2024-10-30

Python Error Handling Explained

If an error occurs during the running of a program, it is possible to agree in advance to return an error code, so that you know if there was an error and what went wrong. Returning an error code is very common in calls provided by the operating system. For example, the function open(), which opens a file, returns the file descriptor (which is an integer) on success and -1 on error.

The use of error codes to indicate whether or not an error has been made is inconvenient because the normal result that should be returned by the function itself is mixed with the error code, resulting in a large amount of code that the caller must use to determine whether or not an error has been made:

Copy Code The code is as follows.

def foo():
    r = some_function()
    if r==(-1):
        return (-1)
    # do something
    return r

def bar():
    r = foo()
    if r==(-1):
        print 'Error'
    else:
        pass


Once an error is made, there is one more level to report until some function can handle the error (e.g., output an error message to the user).

So high-level languages usually have a built-in set of try... ...except... .finally... error handling mechanisms, and Python is no exception.

try

Let's take a look at the try mechanism with an example:

Copy Code The code is as follows.

try:
    print 'try...'
    r = 10 / 0
    print 'result:', r
except ZeroDivisionError, e:
    print 'except:', e
finally:
    print 'finally...'
print 'END'

When we think that some code may be wrong, we can use try to run this code, if the execution of the error, the subsequent code will not continue to execute, but directly jump to the error handling code, that is, the except statement block, after the execution of the except, if there is a finally statement block, then the execution of the finally statement block, so far, the execution is complete.

The code above produces a division error when calculating 10 / 0:

Copy Code The code is as follows.

try...
except: integer division or modulo by zero
finally...
END

As you can see from the output, when an error occurs, the subsequent statement print 'result:', r is not executed and except is executed because it catches a ZeroDivisionError. Finally, the finally statement is executed. The program then continues down the flow.

If the divisor 0 is changed to 2, the execution results are as follows:

Copy Code The code is as follows.

try...
result: 5
finally...
END

The except statement block will not be executed since no error has occurred, but finally will definitely be executed if there is one (there can be no finally statement).

You can also guess that there should be many types of errors, and if different types of errors occur, they should be handled by different blocks of except statements. That's right, there can be multiple excepts to catch different types of errors:

Copy Code The code is as follows.

try:
    print 'try...'
    r = 10 / int('a')
    print 'result:', r
except ValueError, e:
    print 'ValueError:', e
except ZeroDivisionError, e:
    print 'ZeroDivisionError:', e
finally:
    print 'finally...'
print 'END'

The int() function may throw ValueError, so we catch ValueError with one except and ZeroDivisionError with another.

In addition, if no error occurs, you can add an else after the except statement block, which will automatically execute the else statement when no error occurs:

Copy Code The code is as follows.

try:
    print 'try...'
    r = 10 / int('a')
    print 'result:', r
except ValueError, e:
    print 'ValueError:', e
except ZeroDivisionError, e:
    print 'ZeroDivisionError:', e
else:
    print 'no error!'
finally:
    print 'finally...'
print 'END'

Python's error is actually class, all error types are inherited from BaseException, so in the use of except need to pay attention to is that it not only captures the type of error, but also its subclasses are also "a net". For example:
Copy Code The code is as follows.

try:
    foo()
except StandardError, e:
    print 'StandardError'
except ValueError, e:
    print 'ValueError'

The second except will never catch ValueError because ValueError is a subclass of StandardError, and if it did, it was caught by the first except.

All Python errors are derived from the BaseException class, see here for common error types and inheritance relationships:
/2/library/#exception-hierarchy

Using try... .except to catch errors has another huge benefit, which is that it can be used across multiple layers of calls, for example, if the function main() calls foo(), and foo() calls bar(), and it turns out that bar() has an error, then it can be handled as long as main() catches it:

Copy Code The code is as follows.

def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    try:
        bar('0')
    except StandardError, e:
        print 'Error!'
    finally:
        print 'finally...'


That is, there is no need to catch errors at every possible place where they can go wrong, just at the right level. This greatly reduces the need to write try... . except... ...finally.

call stack

If the error isn't caught, it just keeps going up the chain and eventually gets caught by the Python interpreter, which prints an error message and the program exits. Take a look:

Copy Code The code is as follows.

# :
def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    bar('0')

main()


Executed with the following results:
Copy Code The code is as follows.

$ python
Traceback (most recent call last):
  File "", line 11, in <module>
    main()
  File "", line 9, in main
    bar('0')
  File "", line 6, in bar
    return foo(s) * 2
  File "", line 3, in foo
    return 10 / int(s)
ZeroDivisionError: integer division or modulo by zero

It's not scary to make an error, it's scary to not know what went wrong. Interpreting the error message is the key to locating the error. We can see the entire chain of error calling functions from top to bottom:

Error message line 1:

Copy Code The code is as follows.

Traceback (most recent call last):

Tell us it's the wrong tracking information.

Line 2:

Copy Code The code is as follows.

  File "", line 11, in <module>
    main()

There is an error calling main(), at line 11 of the code file code, but the reason is line 9:
Copy Code The code is as follows.

  File "", line 9, in main
    bar('0')

Calling bar('0') is wrong, at line 9 of the code file code, but the reason is line 6:
Copy Code The code is as follows.

  File "", line 6, in bar
    return foo(s) * 2

The reason for this is that the statement return foo(s) * 2 is wrong, but that's not the final reason, read on:
Copy Code The code is as follows.

  File "", line 3, in foo
    return 10 / int(s)

The reason for this is an error in the statement return 10 / int(s), which is the source of the error as it is printed below:
Copy Code The code is as follows.

ZeroDivisionError: integer division or modulo by zero

According to the error type ZeroDivisionError, we determine that int(s) itself is not wrong, but int(s) returns 0, and there is an error in the calculation of 10 / 0. Thus, the source of the error is found.

recording error

If we didn't capture the error, we could naturally have the Python interpreter print out the error stack, but the program would also be terminated. Since we can capture the error, we can print out the error stack and then analyze the cause of the error and, at the same time, let the program continue to execute.

Python's built-in logging module makes it very easy to log error messages:

Copy Code The code is as follows.

#
import logging

def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    try:
        bar('0')
    except StandardError, e:
        (e)

main()
print 'END'


Again, there is an error, but the program will continue to execute and exit normally after printing the error message:
Copy Code The code is as follows.

$ python
ERROR:root:integer division or modulo by zero
Traceback (most recent call last):
  File "", line 12, in main
    bar('0')
  File "", line 8, in bar
    return foo(s) * 2
  File "", line 5, in foo
    return 10 / int(s)
ZeroDivisionError: integer division or modulo by zero
END

Logging can also be configured to log errors to a log file for easy troubleshooting after the fact.

throw an error

Because errors are classes, catching an error is catching an instance of that class. Thus, errors are not created out of thin air, but are intentionally created and thrown.Python's built-in functions throw many types of errors, and functions we write ourselves can also throw errors.

If you want to throw an error, first, as needed, you can define an error class, choose the inheritance relationship, and then, use a raise statement to throw an instance of the error:

Copy Code The code is as follows.

#
class FooError(StandardError):
    pass

def foo(s):
    n = int(s)
    if n==0:
        raise FooError('invalid value: %s' % s)
    return 10 / n


execution, which can end up tracking down our own self-defined errors:
Copy Code The code is as follows.

$ python
Traceback (most recent call last):
  ...
__main__.FooError: invalid value: 0

Define our own error types only when necessary. Try to use Python's existing built-in error types (e.g. ValueError, TypeError) if you can choose them.

Finally, let's look at another way of handling errors:

Copy Code The code is as follows.

#
def foo(s):
    n = int(s)
    return 10 / n

def bar(s):
    try:
        return foo(s) * 2
    except StandardError, e:
        print 'Error!'
        raise

def main():
    bar('0')

main()


In the bar() function, we've obviously caught the error, but, after printing an Error! and then throwing the error out via a raise statement, isn't that sick?

In fact, this type of error handling is not only not sick, but quite common. The purpose of catching the error is simply to record it for subsequent tracking. However, since the current function doesn't know what to do with the error, the most appropriate way to handle it is to continue to throw it upwards and let the top-level caller handle it.

The raise statement throws the current error as is if it takes no arguments. In addition, raising an Error in except converts one type of error to another:

Copy Code The code is as follows.

try:
    10 / 0
except ZeroDivisionError:
    raise ValueError('input error!')

Conversion logic is fine as long as it makes sense, but an IOError should never be converted to an unrelated ValueError.

wrap-up

Python's built-in try... .except... . finally is very handy for handling errors. When errors occur, it is critical to analyze the error message and locate the code where the error occurred.

Programs can also actively throw errors and let the caller deal with them accordingly. However, it should be clear in the documentation what errors may be thrown and why they are thrown.