Error handling

Tips for navigating the slides:
  • Press O or Escape for overview mode.
  • Visit this link for a nice printable version
  • Press the copy icon on the upper right of code blocks to copy the code

Class outline:

  • Review
  • Syntax errors & linting
  • Logic errors & testing
  • Runtime errors & exception handling

Review: OS and argparse

OS module

The OS module is part if the standard Python library and contains loads of functions for interacting with a computer's operating system, including:

  • Creating directories
  • Renaming/deleting files & folders
  • Listing files in a directory
  • Setting file permissions
  • Getting and setting environment variables
  • Managing processes

Create directories

Create a directory using os.makdir()


                        import os
                        os.mkdir('./data')
                        # now we can create a new file inside our dir
                        authors = ["Ursula K. Le Guin", "N. K. Jemisin", "Octavia E. Butler"]
                        with open("data/authors.txt", "a") as file:
                            for author in authors:
                                file.write(author + "\n")
                    

List files in a directory

Check what's inside a directory using os.listdir().
This function returns a list of filenames as strings.


                        import os
                        files = os.listdir('./data')
                        for file in file:
                            print(file)
                    

OS.path module

The os.path module is also part if the standard Python library and contains handy functions for getting information about files, including:

  • Checking if a file/directory exists
  • Checking if a thing is a file or a directory
  • Extracting parts of a filepath, such as the parent directory, the filename or the fiel extension

Check if a file/directory exists

If we try to create a directory that already exists, we get an error.
It's a good practice to check whether a directory exists before creating a new one using os.path.exists()

os.path.exists() returns True or False


                        import os
                        if not os.path.exists('./data'):
                            os.mkdir('./data')
                    

Get the file extension

Sometimes you need to get just file names or extensions. Use os.path.splitext() to return a list with 2 items: the file name and the extension


                        import os
                        files = os.listdir('./data')
                        for file in file:
                            split = os.path.splitext(file)
                            name = split[0]
                            print(name)
                            extension  = split[1]
                            print(extension)
                    

argparse module

The argparse module is part of the standard Python library and contains functions that allow accepting and processing command line arguments, as well as providing help text to users.

Defining arguments

add_argument() is used to define a new argument.
A name is required, and additional parameters can also be set, such as:

  • type automatically convert an argument to the given type (str, int, float, etc). Default is str.
  • required whether an argument is required or optional (True or False). Default is False.
  • help message for the script user about the argument
  • choices limit values to a specific set of choices (formatted as a list or range, ex ['foo', 'bar'])

Defining arguments


                        import argparse
                        # initialize the argument parser
                        parser=argparse.ArgumentParser()
                        # define an argument
                        parser.add_argument('-d', '--directory',
                                            type=str,
                                            required=True,
                                            help='The directory that report files are located in')
                    

Note: 1 argument can have multiple names. It's common to support both a short -d and long --directory name version. - and -- before short and long names are not required, but they are common convention in CLI commands.

Using argument values

parse_args() is used to extract values passed in as arguments


                        import argparse
                        # initialize the argument parser
                        parser=argparse.ArgumentParser()
                        # define an argument
                        parser.add_argument('-d', '--directory',
                                            type=str,
                                            required=True,
                                            help='The directory that report files are located in')
                        # parse the arguments
                        args=parser.parse_args()
                        # get a specifc argument value
                        directory = args.directory
                    

Multiple arguments

Can we have multiple arguments? Of course!


                        import argparse
                        # initialize the argument parser
                        parser=argparse.ArgumentParser()
                        # define an argument
                        parser.add_argument('-d', '--directory',
                                            type=str,
                                            required=True,
                                            help='The directory that report files are located in')
                        parser.add_argument('-n', '--name',
                                            type=str,
                                            required=True,
                                            help='Your name')
                        # parse the arguments (parse_args() parses all arguments)
                        args=parser.parse_args()
                        # get a specifc argument value
                        directory = args.directory
                        name = args.name
                    

Passing arguments in Replit

The Replit Run button does the equivalent of typing python main.py into a command line interface (CLI).

We can access the CLI in Replit by switching the right panel from Console to Shell

Passing arguments in Replit

screenshot showing replit shell

Exercise review

Errors

Types of errors

These are common to all programming languages:

  • Syntax errors
  • Logic errors
  • Runtime errors

Syntax errors

Syntax errors

Each programming language has syntactic rules. If the rules aren't followed, the program cannot be parsed and will not be executed at all.

Spot the syntax errors:


                    if x > 5 # Missing colon
                       x += 1
                    

                    sum = 0
                    for x in range(0, 10):
                       sum + = x # No space needed between + and =
                    

                        def my_func(num_1, num_2):
                           sum = num_1 + num_2
                        return sum # Return is not indented
                        

Common syntax errors

  • Extra or missing parenthesis
  • Missing colon at the end of an if, while, def statements, etc.
  • Missing/unmatched quotes
  • Incorrect/misaligned indents

To fix a syntax error, read the message carefully and go through your code with a critical eye. πŸ‘
...and lint your code!

Linting

To avoid syntax errors, use a linting tool to check your code as you write it. Many IDEs have a Python linter built in or available as an extension (ex: VSCode Python linting).

You can also run Pylint on your code


                    # import pylint
                    # Already installed in Replit - no import needed
                    def sum(a, b):
                        total = a + b
                    return total
                    

In Replit, switch to the Shell tab and type pylint main.py and press Enter.
When prompted to select a version, press Enter again.

Exercise

Fix this code!

Class9Ex1SyntaxErrors

Logic errors

Logic errors

A program has a logic error if it does not behave as expected. Typically discovered via failing tests or bug reports from users.

Spot the logic error:


                    # Which medal goes to the first place winner?
                    medals = ["gold", "silver", "bronze"]
                    first_place = medals[1]
                    print(first_place)
                    

To avoid the wrath of angry users due to logic errors, write tests!

Testing

There are several Python testing frameworks. Pytest is a good one to start with.


                    import pytest

                    medals = ["gold", "silver", "bronze"]
                    def get_medal(place):
                        return medals[place]

                    def test_get_medal():
                        assert get_medal(1) == "gold"
                    

In Replit, click Run to install Pytest, then switch to the Shell tab and type pytest main.py and press Enter.

Exercise: Pytest

Add a test with Pytest

Class9Ex2Pytest

Runtime errors

Runtime errors

Even with testing and linting, errors can slip through!

A runtime error happens while a program is running, often halting the execution of the program.

Spot the runtime error:


                    def div_numbers(dividend, divisor):
                        return dividend/divisor

                    quot1 = div_numbers(10, 2)
                    quot2 = div_numbers(10, 1)
                    quot3 = div_numbers(10, 0)  # Cannot divide by 0!
                    quot4 = div_numbers(10, -1)
                    

To prevent runtime errors, code defensively and write tests for all edge cases.

NameError

What it technically means:
Python looked up a name but couldn't find it

What it practically means:

  • You made a typo

What you should look for:

  • A typo in the name

Example:


                    fav_nut = 'pistachio'
                    best_chip = 'chocolate'
                    trail_mix = Fav_Nut + best__chip
                    

...NoneType...

What it technically means:
You used None (null) in some operation it wasn't meant for

What it practically means:
You forgot a return statement in a function

What you should look for:

  • Functions missing return statements
  • Printing instead of returning a value

Example:


                    def sum(a, b):
                        print(a + b)

                    total = sum( sum(30, 45), sum(10, 15) )
                    

TypeError:'X' object is not callable

What it technically means:
Objects of type X cannot be treated as functions

What it practically means:
You accidentally called a non-function as if it were a function

What you should look for:

  • Parentheses after variables that aren't functions
  • Variables with same name as a function

Example:


                    sum = 2 + 2
                    sum(3, 5)
                    

TraceBacks

What's a traceback?

When there's a runtime error in your code, you'll see a traceback in the console.


                        def div_numbers(dividend, divisor):
                            return dividend/divisor

                        quot1 = div_numbers(10, 2)
                        quot2 = div_numbers(10, 1)
                        quot3 = div_numbers(10, 0)
                        quot4 = div_numbers(10, -1)
                    

                    Traceback (most recent call last):
                        File "main.py", line 14, in <module>
                            quot3 = div_numbers(10, 0)
                        File "main.py", line 10, in div_numbers
                            return dividend/divisor
                    ZeroDivisionError: division by zero
                    

Parts of a Traceback

  • The error message itself
  • Lines #s on the way to the error
  • What’s on those lines

The most recent line of code is always last (right before the error message).


                    Traceback (most recent call last):
                        File "main.py", line 14, in <module>
                            quot3 = div_numbers(10, 0)
                        File "main.py", line 10, in div_numbers
                            return dividend/divisor
                    ZeroDivisionError: division by zero
                    

Reading a Traceback

  1. Read the error message (remember what common error messages mean!)
  2. Look at each line, bottom to top, and see if you can find the error.

                    Traceback (most recent call last):
                        File "main.py", line 14, in <module>
                            quot3 = div_numbers(10, 0)
                        File "main.py", line 10, in div_numbers
                            return dividend/divisor
                    ZeroDivisionError: division by zero
                    

Exceptions

Exceptions

Python raises an exception whenever a runtime error occurs. An exception is a mechanism in many languages used to declare and respond to "exceptional" conditions.

How an exception is reported:


                    >>> 10/0
                    Traceback (most recent call last):
                        File "<stdin>", line 1, in 
                    ZeroDivisionError: division by zero
                    

If an exception is not handled, the program stops executing immediately.

The try statement

To handle an exception (keep the program running), use a try statement.


                    try:
                        <code you want to run if all goes well>
                    except:
                        <code you want to run if this particular exception happens>
                       ...
                    

The <try statement> is executed first. If and exception is raised with the <try statement> executes, the <except statement> is executed.

Try statement example


                    try:
                        quot = 10/0
                    except:
                        print("An error happened")
                        quot = 0
                    

Getting error details

To get the error message that Python returned, we need to catch the Exception and assign it to a variable.


                    try:
                        quot = 10/0
                    except Exception as e:
                        print('Handling an error:', e)
                        quot = 0
                    

Default Exception

All exception types are derived from the parent class Exception.

  • Exception can be used as a wildcard that catches (almost) everything
  • It can be used on its own, or as a last resort to catch exceptions that you haven't caught with a more specific type

Types of exceptions

A few exception types and examples of buggy code:

Exception Example
TypeError 'hello'[1] = 'j'
IndexError 'hello'[7]
NameError x += 5
FileNotFoundError open('dsfdfd.txt')

See full list in the exceptions docs. Note: Third party Python packages often include their own custom exception types!

Catching a specific exception type

In some cases, we need to catch and handle specific exceptions. Multiple except statements that catch different exception types are allowed. It's a best practice to catch the default Exception, in addition to specific types.


                    try:
                        quot = 10/0
                    except ZeroDivisionError as e:
                        print('Handling zero division exception:', e)
                        quot = 0
                    except Exception as e:
                        print('Something else went wrong:', e)
                    

Try inside a function


                    def div_numbers(dividend, divisor):
                        try:
                            quotient = dividend/divisor
                        except ZeroDivisionError as e:
                            print('Handling zero division exception:', e)
                            quotient = 0
                        except Exception as e:
                            print('Something else went wrong:', e)
                            quotient = None
                        return quotient

                    div_numbers(10, 2)
                    div_numbers(10, 0)
                    div_numbers(10, 'foo')
                    

Depending on the case, we may want to place the return statement inside the try statement, so that there is only a return value if the code succeeds.

System Exit

To stop the program if a certain exception is encountered, move the return statement inside the try statement and raise a SystemExit() inside the except statement.


                    def div_numbers(dividend, divisor):
                        try:
                            quotient = dividend/divisor
                        except ZeroDivisionError as e:
                            print('Encountered error:', e)
                            quotient = 0
                        except Exception as e:
                            print('Something else went wrong:', e)
                            raise SystemExit()
                        return quotient

                    div_numbers(10, 2)
                    div_numbers(10, 0)
                    div_numbers(10, -1)
                    

Exercise: Try/Except

Let's make our report code a bit more robust with some error handling

Class9Ex3TryExcept