Let's talk about exception handling in Python.
Here we have a program called find_todos.py:
from argparse import ArgumentParser
from pathlib import Path
def count_in_file(path, string):
count = 0
with open(path) as text_file:
for line in text_file:
if string in line:
count += 1
return count
def main():
parser = ArgumentParser()
parser.add_argument("paths", type=Path, nargs="+")
args = parser.parse_args()
for path in args.paths:
todos = count_in_file(path, "TODO")
if todos:
print(f"{path}: {todos} TODOs found")
if __name__ == "__main__":
main()
When this program is running from the command-line, it will accept any number of arguments we can give to it, and it'll assume that they're all file paths:
def main():
parser = ArgumentParser()
parser.add_argument("paths", type=Path, nargs="+")
args = parser.parse_args()
It'll loop over those given file paths, counting the number of lines that contain the string TODO:
for path in args.paths:
todos = count_in_file(path, "TODO")
if todos:
print(f"{path}: {todos} TODOs found")
If TODO was found in this file, it'll print out the number of TODOs that were found, and it'll continue onward looping over the other file paths that were given.
If we run this program with a couple filenames, it will print out the number of TODOs that were found in these files (if one or more was found):
~/my_project $ python3 find_todos.py setup.py README.md
README.md: 1 TODOs found
In README.md, one TODO that was found.
If we run this program with something that isn't a valid filename (a directory for example) we'll get an error and a traceback will be printed out:
~/my_project $ python3 find_todos.py setup.py src README.md
Traceback (most recent call last):
File "/home/trey/my_project/find_todos.py", line 25, in <module>
main()
File "/home/trey/my_project/find_todos.py", line 19, in main
todos = count_in_file(path, "TODO")
File "/home/trey/my_project/find_todos.py", line 7, in count_in_file
with open(path) as text_file:
IsADirectoryError: [Errno 21] Is a directory: 'src'
A traceback was printed out because an exception was raised in our code, and that exception went unhandled.
When a directory is given, it would be nice to print out an error message and then continue onward, printing out the number of TODOs for all the other given files.
To handle this specific exception, we could start on line 7, because that's where the exception was actually raised in our code:
File "/home/trey/my_project/find_todos.py", line 7, in count_in_file
with open(path) as text_file:
IsADirectoryError: [Errno 21] Is a directory: 'src'
The exception occurred when we called the built-in open function in our count_in_file function:
def count_in_file(path, string):
count = 0
with open(path) as text_file:
for line in text_file:
if string in line:
count += 1
return count
But this count_in_file function isn't actually a great place to handle this exception, because we're returning from this function.
We probably shouldn't print an error and return within the same function: that's not the job of this function.
try-except blockWe should probably go one more level up in our call stack (remember that's what a traceback represents):
File "/home/trey/my_project/find_todos.py", line 19, in main
todos = count_in_file(path, "TODO")
File "/home/trey/my_project/find_todos.py", line 7, in count_in_file
with open(path) as text_file:
IsADirectoryError: [Errno 21] Is a directory: 'src'
Line 19 is the next level up in our code, where that count_in_file function was actually called:
def main():
parser = ArgumentParser()
parser.add_argument("paths", type=Path, nargs="+")
args = parser.parse_args()
for path in args.paths:
todos = count_in_file(path, "TODO")
if todos:
print(f"{path}: {todos} TODOs found")
So when our count_in_file function is called, we'll catch this exception by using a try-except block:
for path in args.paths:
try:
todos = count_in_file(path, "TODO")
except IsADirectoryError:
print(f"Error: {path} is a directory")
continue
if todos:
print(f"{path}: {todos} TODOs found")
First we try to run that one line of code (where we call the count_in_file function).
If an IsADirectoryError occurs we print out an error (stating that the given path is a directory) and then we continue to the next iteration of our for loop (skipping over the current path, and handling the next path):
We're catching an IsADirectoryError because that's the type of exception that that was raised, as shown in the last line in our traceback:
IsADirectoryError: [Errno 21] Is a directory: 'src'
Now if we run this code again, it works:
~/my_project $ python3 find_todos.py setup.py src README.md
Error: src is a directory
README.md: 1 TODOs found
It says Error: src is a directory, and then it continues onward, finding that there are TODOs in README.md.
try-except in Python instead of try-catchTo catch an exception in Python, you'll first need to identify the line that you need to handle that exception on (the line that raised the exception).
Then you'll want to put that line in a try block, using an except block to note the specific type of exception that was raised, and then noting the block of code that should be run to handle that exception.
Python Jumpstart is designed to help new Python programmers get up to speed quickly. Get hands-on practice with 50 bite-sized modules that build your Python skills step by step.
Sign up for my 5 day email course and learn essential concepts that introductory courses often overlook!
Sign in to your Python Morsels account to track your progress.
Don't have an account yet? Sign up here.
Sign up for my free 5 day email course and learn essential concepts that introductory courses often overlook: iterables, callables, pointers, duck typing, and namespaces. Learn to avoid beginner pitfalls, in less than a week!
Ready to level up? Sign up now to begin your Python journey the right way!