Defensive Programming
Last updated on 2026-01-16 | Edit this page
Overview
Questions
- How do I predict and avoid user confusion?
Objectives
- Ensure that programs indicate use and provide meaningful output upon failure.
Defensive Programming
In our last lesson, we created a program which will plot our gapminder gdp data for an arbitrary number of files. This is great, but we didn’t cover some of the vulnerabilities of this program we’ve created.
- What happens if we run the program without any arguments at all?
- What happens if we run the program from another directory?
First, let’s try running our program without any additional arguments or flags.
It gives no output at all and if we check the figs folder
None of the files have been overwritten this time.
Python reports a runtime error when something goes wrong while a program is executing.
ERROR
NameError: name 'aege' is not defined
- The message indicates a problem with the name of a variable
Python also reports a syntax error when it can’t understand the source of a program.
ERROR
File "<ipython-input-6-d1cc229bf815>", line 1
print ("hello world"
^
SyntaxError: unexpected EOF while parsing
- The message indicates a problem on first line of the input (“line
1”).
- In this case the “ipython-input” section of the file name tells us that we are working with input into IPython, the Python interpreter used by the Jupyter Notebook.
- The
-6-part of the filename indicates that the error occurred in cell 6 of our Notebook. - Next is the problematic line of code, indicating the problem with a
^pointer.
And if we run the program from another directory:
We see no output from the program at all for either of these cases. This is what is referred to as a “silent failure”. The program has failed to produce a plot, but has reported no reason why. These kind of failures are difficult to debug and should be avoided.
It is important to employ “defensive programming” in this scenario so that our program indicates to the user
- what is going wrong
- how to correct this problem
Check Input Arguments
Let’s add a section to the code which checks the number of incoming arguments to the program and returns some information to the user if there is missing information.
import sys
import glob
import pandas
# we need to import part of matplotlib
# because we are no longer in a notebook
import matplotlib.pyplot as plt
# make sure additional arguments or flags have
# been provided by the user
if len(sys.argv) == 1:
# why the program will not continue
print("Not enough arguments have been provided")
# how this can be corrected
print("Usage: python gdp_plots.py \< filenames \>")
print("Options:")
print("-a : plot all gdp data sets in current directory")
# check for -a flag in arguments
if "-a" in sys.argv:
filenames = glob.glob("data/*gdp*.csv")
else:
filenames = sys.argv[1:]
for filename in filenames:
# load data and transpose so that country names are
# the columns and their gdp data becomes the rows
data = pandas.read_csv(filename, index_col = 'country').T
if "continent" in data.index:
data.drop("continent", inplace=True)
# create a plot of the transposed data
ax = data.plot(title = filename)
# set some plot attributes
ax.set_xlabel("Year")
ax.set_ylabel("GDP Per Capita")
# set the x locations and labels
ax.set_xticks(range(len(data.index)))
ax.set_xticklabels(data.index, rotation = 45)
# save the plot with a unique file name
split_name1 = filename.split('.')[0] #data/gapminder_gdp_XXX
split_name2 = filename.split('/')[1]
save_name = 'figs/'+split_name2 + '.png'
plt.savefig(save_name)
If we run the program without a filename argument, here’s what we’ll see
OUTPUT
Not enough arguments have been provided
Usage: python gdp_plots.py <filenames>
Options:
-a : plot all gdp file in current directory
Now if someone runs this program without having used it before (or written it themselves) the user will know how change their command to get the program running properly, rather than seeing an esoteric Python error.
Adding early exits
While the addition of these print statements works for this
particular script because it is a silent error, if the error was not
silent we would have needed to include an exit() function.
The exit() function will end the program early and we could
include it in our if statement. Then if the if
condition is met and the usage statement runs, the program will end
early. This will prevent users from having to see a potentially
confusing error message in addition to the usage message.
Update the Repository
We’ve just made another successful change to our repository. Let’s add a commit to the repo.
Silent Errors Challenge
Silent errors can be difficult to anticipate. If we try to run our
program from another directory with the -a flag, we still
don’t see any errors, but it also doesn’t do anything. This is because
when we do the -a flag here, there are no .csv
files in the directory, so our filenames list is empty. Add
a check for this case and an error message when the issue arises.
import sys
import glob
import pandas
# we need to import part of matplotlib
# because we are no longer in a notebook
import matplotlib.pyplot as plt
# make sure additional arguments or flags have
# been provided by the user
if len(sys.argv) == 1:
# why the program will not continue
print("Not enough arguments have been provided")
# how this can be corrected
print("Usage: python gdp_plots.py \< filenames \>")
print("Options:")
print("-a : plot all gdp data sets in current directory")
# check for -a flag in arguments
if "-a" in sys.argv:
filenames = glob.glob("data/*gdp*.csv")
# check if no files were found and print message.
if filenames == []:
# file list is empty (no files found)
print("No files found in this folder.")
print("Make sure data is located in current directory.")
else:
filenames = sys.argv[1:]
for filename in filenames:
# load data and transpose so that country names are
# the columns and their gdp data becomes the rows
data = pandas.read_csv(filename, index_col = 'country').T
if "continent" in data.index:
data.drop("continent", inplace=True)
# create a plot of the transposed data
ax = data.plot(title = filename)
# set some plot attributes
ax.set_xlabel("Year")
ax.set_ylabel("GDP Per Capita")
# set the x locations and labels
ax.set_xticks(range(len(data.index)))
ax.set_xticklabels(data.index, rotation = 45)
# save the plot with a unique file name
split_name1 = filename.split('.')[0] #data/gapminder_gdp_XXX
split_name2 = filename.split('/')[1]
save_name = 'figs/'+split_name2 + '.png'
plt.savefig(save_name)
Now if someone runs this program in a directory with no valid datafiles, a message appears.