During program execution, if an error occurs, we can predefine an error code to return. This allows us to determine whether an error has occurred and identify its cause. Returning error codes is very common in operating system-provided calls. For example, the open() function (used to open files) returns a file descriptor (an integer) on success, and -1 on failure.
Using error codes to indicate errors is highly inconvenient because the normal return value of a function is mixed with error codes, forcing the caller to write extensive code to check for errors:
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 occurs, it must be reported level by level until a function can handle it (e.g., output an error message to the user).
For this reason, high-level languages typically include a built-in error handling mechanism: try...except...finally...—and Python is no exception.
Let’s use an example to understand how try works:
try:
print('try...')
r = 10 / 0
print('result:', r)
except ZeroDivisionError as e:
print('except:', e)
finally:
print('finally...')
print('END')
When we anticipate that certain code may throw an error, we wrap it in a try block. If an error occurs during execution, subsequent code in the try block stops running, and the program jumps directly to the error handling code (the except block). After executing the except block (if present), the finally block (if present) is executed, marking the end of the error handling process.
The code above generates a division error when calculating 10 / 0:
try...
except: division by zero
finally...
END
From the output, we see that when the error occurs, the subsequent statement print('result:', r) is not executed. The except block runs because it catches the ZeroDivisionError. Finally, the finally block is executed, and the program continues running as normal.
If we change the divisor from 0 to 2, the output becomes:
try...
result: 5
finally...
END
Since no error occurs, the except block is skipped, but the finally block (if present) always runs (the finally block is optional).
You might also guess that errors come in many types—and different types of errors should be handled by different except blocks. This is correct: multiple except blocks can be used to catch different error types:
try:
print('try...')
r = 10 / int('a')
print('result:', r)
except ValueError as e:
print('ValueError:', e)
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
finally:
print('finally...')
print('END')
The int() function may throw a ValueError, so we use one except block to catch ValueError and another to catch ZeroDivisionError.
Additionally, if no errors occur, we can add an else block after the except blocks—it runs automatically when no errors are raised:
try:
print('try...')
r = 10 / int('2')
print('result:', r)
except ValueError as e:
print('ValueError:', e)
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
else:
print('no error!')
finally:
print('finally...')
print('END')
In Python, errors are actually classes—all error types inherit from BaseException. When using except, note that it catches not only the specified error type but also all its subclasses. For example:
try:
foo()
except ValueError as e:
print('ValueError')
except UnicodeError as e:
print('UnicodeError')
The second except block will never catch UnicodeError, because UnicodeError is a subclass of ValueError—any UnicodeError would already be caught by the first except block.
All Python errors derive from the BaseException class. For a list of common error types and their inheritance hierarchy, see:
https://docs.python.org/3/library/exceptions.html#exception-hierarchy
A major advantage of using try...except for error handling is that it works across multiple levels of function calls. For example, if main() calls bar(), bar() calls foo(), and foo() throws an error, main() can catch and handle it:
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar('0')
except Exception as e:
print('Error:', e)
finally:
print('finally...')
In other words, we don’t need to catch errors at every possible failure point—only at the appropriate level. This significantly reduces the overhead of writing try...except...finally blocks.
If an error is not caught, it propagates upward until it is caught by the Python interpreter, which prints an error message and terminates the program. Let’s examine err.py:
# err.py:
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
bar('0')
main()
Execute it, and the output is:
$ python3 err.py
Traceback (most recent call last):
File "err.py", line 11, in <module>
main()
File "err.py", line 9, in main
bar('0')
File "err.py", line 6, in bar
return foo(s) * 2
File "err.py", line 3, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
Errors are not scary—what’s scary is not knowing where they occur. Interpreting error messages is key to locating errors. We can trace the entire chain of function calls from top to bottom:
Line 1 of the error message:
Traceback (most recent call last):
This tells us this is the error traceback (tracking information for the error).
Lines 2–3:
File "err.py", line 11, in <module>
main()
The call to main() failed at line 11 of the code file err.py, but the root cause is line 9:
File "err.py", line 9, in main
bar('0')
The call to bar('0') failed at line 9 of the code file err.py, but the root cause is line 6:
File "err.py", line 6, in bar
return foo(s) * 2
The error occurred in the statement return foo(s) * 2—but this is not the final cause. Continue reviewing the traceback:
File "err.py", line 3, in foo
return 10 / int(s)
The error occurred in the statement return 10 / int(s)—this is the root source of the error, as indicated by the following line in the traceback:
ZeroDivisionError: integer division or modulo by zero
Based on the error type ZeroDivisionError, we determine that int(s) itself did not fail—but int(s) returned 0, causing the error when calculating 10 / 0. We have now identified the root cause of the error.
Tip
When an error occurs, always analyze the call stack information to locate the error’s position.
Who taught you to ask questions without pasting the exception stack?
If we don’t catch an error, the Python interpreter prints the stack trace and terminates the program. Since we can catch errors, we can print the stack trace, analyze the cause, and allow the program to continue running.
Python’s built-in logging module makes it easy to log error information:
import logging
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar('0')
except Exception as e:
logging.exception(e)
main()
print('END')
Even though an error occurs, the program prints the error message, continues executing, and exits normally:
$ python3 err_logging.py
ERROR:root:division by zero
Traceback (most recent call last):
File "err_logging.py", line 13, in main
bar('0')
File "err_logging.py", line 9, in bar
return foo(s) * 2
File "err_logging.py", line 6, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
END
With configuration, logging can also write errors to a log file for later troubleshooting.
Since errors are classes, catching an error means catching an instance of that class. Errors are not generated randomly—they are intentionally created and raised. Python’s built-in functions throw many types of errors, and we can also raise errors in our own functions.
To raise an error:
raise statement to throw an instance of the error class:
class FooError(ValueError):
pass
def foo(s):
n = int(s)
if n == 0:
raise FooError('invalid value: %s' % s)
return 10 / n
foo('0')
Execute it, and the traceback will point to our custom error:
$ python3 err_raise.py
Traceback (most recent call last):
File "err_throw.py", line 11, in <module>
foo('0')
File "err_throw.py", line 8, in foo
raise FooError('invalid value: %s' % s)
__main__.FooError: invalid value: 0
Define custom error types only when necessary. Whenever possible, use Python’s built-in error types (e.g., ValueError, TypeError).
Finally, let’s look at another error handling pattern:
def foo(s):
n = int(s)
if n == 0:
raise ValueError('invalid value: %s' % s)
return 10 / n
def bar():
try:
foo('0')
except ValueError as e:
print('ValueError!')
raise
bar()
In the bar() function, we catch the error, print ValueError!, then re-raise the error with raise. This may seem counterintuitive—but it is actually a common and valid pattern.
Catching an error is often just for logging purposes (to track the error), but the current function may not know how to handle it. The appropriate approach is to re-raise the error to let the upper-level caller handle it. This is analogous to an employee escalating an unsolvable problem to their manager, who may escalate it further until it reaches the CEO (the top-level caller).
If the raise statement is used without arguments, it re-throws the current error as-is. Additionally, you can convert one error type to another in an except block (as long as the conversion is logical):
try:
10 / 0
except ZeroDivisionError:
raise ValueError('input error!')
Never convert an unrelated error type (e.g., IOError to ValueError)—only convert errors when the new type accurately reflects the issue.
Run the code below, analyze the exception message to locate the root cause, and fix it:
from functools import reduce
def str2num(s):
return int(s)
def calc(exp):
ss = exp.split('+')
ns = map(str2num, ss)
return reduce(lambda acc, x: acc + x, ns)
def main():
r = calc('100 + 200 + 345')
print('100 + 200 + 345 =', r)
r = calc('99 + 88 + 7.6')
print('99 + 88 + 7.6 =', r)
main()
Python’s built-in try...except...finally mechanism provides a convenient way to handle errors. When an error occurs, analyzing the error message and locating the faulty code is the most critical step.
Programs can also proactively raise errors to let callers handle them—but you should clearly document which errors may be raised and their causes.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
try:
print("try...")
r = 10 / 0
print("result:", r)
except ZeroDivisionError as e:
print("except:", e)
finally:
print("finally...")
print("END")
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
bar("0")
main()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar("0")
except Exception as e:
logging.exception(e)
main()
print("END")
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class FooError(ValueError):
pass
def foo(s):
n = int(s)
if n == 0:
raise FooError("invalid value: %s" % s)
return 10 / n
foo("0")
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
def foo(s):
n = int(s)
if n == 0:
raise ValueError("invalid value: %s" % s)
return 10 / n
def bar():
try:
foo("0")
except ValueError as e:
print("ValueError!")
raise
bar()