blog bg

October 19, 2023

Exceptional exceptions - try before your code dies

Share what you learn in this blog to prepare for your interview, create your forever-free profile now, and explore how to monetize your valuable knowledge.

 

 

Purpose

This tutorial will introduce learners and beginners to exception handling in Python. 

Audience

Beginners and learners.  

Prerequisites 

  • In this article, I will use a project from my previous post. It is not essential to have followed this, but it might help
  • You will need an Python interpreter installed - see my introductory post 
  • Text editor or IDE

Introduction

We've all seen them, ugly as ulcers, polluting our consoles and log files. I'm talking about stack traces or worse, meaningless garbage error messages. A stack trace dump is like a trail of breadcrumbs that is scattered, incomplete, and hard to follow. It may be difficult to determine the error's cause, or how to fix it, just by looking at the stack trace. 

It needn't be this way. Most self-respecting programming languages offer most self-respecting developers a way to catch and respond gracefully to errors. We can never avoid errors or crashes altogether, but we can be nice to the users and developers who must live the consequences of our code. 

Let's look at error handling in Python - catch me if you can! 

Step-by-step

Step 1 - Open diff_dates.py

In a previous lesson, we learned to calculate the difference between two dates. For this, we created a file called diff_dates.py. Navigate to the directory in which that file lives. I encourage you to follow that tutorial if you haven't already. However, if you're comfortable skipping head, here's the source from that project.

from datetime import date
import sys

def diff_dates(date1, date2):
    return abs(date2-date1).days

def get_command_line_argument_date():
	return (date(int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3])))

def main():
    date1 = get_command_line_argument_date()
    date2 = date.today()
    number_of_days = diff_dates(date2, date1)
    print("You are :  "+ str(number_of_days / 365) + " old!") 
main()

Step 2 - Run the program correctly 

  • Run the program with the correct arguments : 
$ python diff_dates.py 2010 09 09
You are :  13.084931506849315 old!
  • The above is what we call a 'happy path' - everything worked as expected. However, we provided the date in the correct format - YYYY MM DD

Step 3 - Run the program incorrectly 

  • Let's try this again with an unexpected date format. 
$ python diff_dates.py 2010-09-09
Traceback (most recent call last):
  File "/home/michael/projects/tuts/diff_dates.py", line 15, in <module>
    main()
  File "/home/michael/projects/tuts/diff_dates.py", line 11, in main
    date1 = get_command_line_argument_date()
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/michael/projects/tuts/diff_dates.py", line 8, in get_command_line_argument_date
    return (date(int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3])))
                 ^^^^^^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: '2010-09-09'

Ugly indeed, and not helpful! This is yet a mild example of an impenetrable error message, I've seen far worse. 

Step 4 - Add exception handling

You will note, at the end of that long, brutish error message was the line: "ValueError: invalid literal for int() with base 10:". This is an example of an 'exception'. What it is trying to tell us is that our code "int(sys.argv[1]" expected a string with a number but instead received a character ("-") that it did not expect. 

Python provides a way to 'catch' these errors and deal with them in a more friendly way. 

We do this with a 'try, except' code block.

  • Alter the get_command_line_argument_date function as below.
from datetime import date
import sys

def diff_dates(date1, date2):
    return abs(date2-date1).days

def get_command_line_argument_date():
    try: 
        return (date(int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3])))
    except ValueError: 
        print("I was expecting only numbers, please try again with integer values only")
        exit()

def main():
    date1 = get_command_line_argument_date()
    date2 = date.today()
    number_of_days = diff_dates(date2, date1)
    print("You are :  "+ str(number_of_days / 365) + " old!") 
main()

Let's break down what we've done here.

'try' - tells Python to execute some code but to be on the lookout for an error. 

'except' - tells Python to watch for a certain error and to take some action should it encounter that error. In our example, we print an error message then terminate the program with the 'exit()' command.

  • Run the modified program with the same bad input parameters. It still fails but in a better way. 
$ python diff_dates.py 2010-01-01
I was expecting only numbers, please try again with integer values only

Step 5 - Catch more errors

Programs rarely have only one way to fail. We've intercepted one error, but where else could diff_dates.py crash? Well, it expects three input parameters, a year, a month, and day. What if we only provided two parameters? 

  • Run diff_dates.py with only two arguments, year, and month. 
$ python diff_dates.py 2010 08
Traceback (most recent call last):
  File "/home/michael/projects/tuts/diff_dates.py", line 19, in <module>
    main()
  File "/home/michael/projects/tuts/diff_dates.py", line 15, in main
    date1 = get_command_line_argument_date()
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/michael/projects/tuts/diff_dates.py", line 9, in get_command_line_argument_date
    return (date(int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3])))
                                                         ~~~~~~~~^^^
IndexError: list index out of range

IndexError means that we tried to access an entry in a list with an index greater than the size of that list. What does that mean? Who cares? For our purposes, we just need to tell the user what has gone wrong. 

  • Let's add another 'except' block. 
def get_command_line_argument_date():
    try: 
        return (date(int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3])))
    except ValueError: 
        print("I was expecting only numbers, please try again with integer values only")
        exit()
    except IndexError:
        print("Please provide a date as follows: YYYY MM DD")
        exit()
  • Run it again: 
$ python diff_dates.py 2010 08
Please provide a date as follows: YYYY MM DD

Better, no?

Step 6 - Expect the unexpected

In my career I have learned that despite my best intentions, my software perpetually seeks new ways to fail. In an enterprise situation, there are too many unknowns, user actions, server features, OS events, and data quirks that conspire to assassinate my beautiful code. We are only human and cannot foresee every scenario. We can decorate our code with except blocks but inevitably, some library somewhere will throw an exception we did not expect. It is what it is. 

Consider the scenario below. It should never happen, but it is conceivable that a user may inadvertently enter a value that cannot be converted to a date. 

python diff_dates.py 99999999999999999999999999999 09 09
Something else went wrong
Traceback (most recent call last):
  File "/home/michael/projects/tuts/diff_dates.py", line 24, in <module>
    main()
  File "/home/michael/projects/tuts/diff_dates.py", line 22, in main
    number_of_days = diff_dates(date2, date1)
                     ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/michael/projects/tuts/diff_dates.py", line 5, in diff_dates
    return abs(date2-date1).days
               ~~~~~^~~~~~
TypeError: unsupported operand type(s) for -: 'NoneType' and 'datetime.date'

Our diff_dates function threw this error because we tried to subtract a crazy date from today's date. Python's date library has a multitude of possible exceptions, and it might not be practical to address them all. For this, we have the generic 'except'. 

  • We can bullet-proof our diff_dates and get_command_line_arguments functions against the unexpected.
def diff_dates(date1, date2):
    try: 
        return abs(date2-date1).days
    except:
        print("Something went wrong with the provided date, please check your input and try again.")
        exit()

def get_command_line_argument_date():
    try: 
        return (date(int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3])))
    except ValueError: 
        print("I was expecting only numbers, please try again with integer values only")
        exit()
    except IndexError:
        print("Please provide a date as follows: YYYY MM DD")
        exit()
    except:
        print("Something went wrong with the provided date, please check your input and try again.")
        exit()
  • Run it again with the bad input: 
$ python diff_dates.py 99999999999999999999999999999 09 09
Something went wrong with the provided date, please check your input and try again.

Step 7 - And Finally 

There are two more 'blocks' we haven't looked at: 'else' and 'finally'.

'else' - executes code when there is no error. 

'finally' - executes code regardless of the outcome in the 'try' block. 

  • Let's finish our program: 
from datetime import date
import sys

def diff_dates(date1, date2):
    try: 
        return abs(date2-date1).days
    except:
        print("Something went wrong with the provided date, please check your input and try again.")
        exit()

def get_command_line_argument_date():
    try: 
        return (date(int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3])))
    except ValueError: 
        print("I was expecting only numbers, please try again with integer values only")
        exit()
    except IndexError:
        print("Please provide a date as follows: YYYY MM DD")
        exit()
    except:
        print("Something went wrong with the provided date, please check your input and try again.")
        exit()
    finally: 
        print("Better or worse, we have your inputs.")        

def main():
    date1 = get_command_line_argument_date()
    date2 = date.today()
    number_of_days = diff_dates(date2, date1)
    print("You are :  "+ str(number_of_days / 365) + " old!") 
main()
  • try again with either bad or good values
$ python diff_dates.py 2010 09 09
Better or worse, we have your inputs.
You are :  13.087671232876712 old!

 

Next steps

We have scratched the surface. There is far more to learn about . For now though, this tutorial will have gifted you with the superpower or writing friendly, maintainable code. 

895 views

Please Login to create a Question