DEV Community

Mark Edosa
Mark Edosa

Posted on

Introduction to Python Programming - Errors and Exceptions

Table of Contents

Introduction

As you write code, errors may occur due to wrong syntax or program execution. The former is known as a syntax error, the latter exception. An unhandled exception will cause your program to crash. Therefore, it is necessary to understand and handle them.

This article focuses on exceptions. Syntax errors are already visible mistakes.

Common Exceptions

Some exceptions are visible to the user. These include NameError, KeyError, IndexError, and so on. Other exceptions are not. These include but are not limited to StopIteration and GeneratorExit exceptions.

Below are some of the exceptions you are likely to encounter.

AssertionError

When an assert statement fails, it raises an assertion error. For example:

assert (4 + 5) == 8
Enter fullscreen mode Exit fullscreen mode
Traceback (most recent call last):
  File "C:\articles\python\introduction\codes\exceptions.py", line 1, in <module>
    assert (4 + 5) == 8
AssertionError
Enter fullscreen mode Exit fullscreen mode

You can customize the message associated with an assertion error. For example:

assert 7 == 8, 'Not sure 7 is equal to 8'
Enter fullscreen mode Exit fullscreen mode
Traceback (most recent call last):
  File "C:\articles\python\introduction\codes\exceptions.py", line 3, in <module>
    assert 7 == 8, 'Not sure 7 is equal to 8'
AssertionError: Not sure 7 is equal to 8
Enter fullscreen mode Exit fullscreen mode

Use the assert statement to ensure your function meets a particular condition during unit testing with a package such as pytest.

AttributeError

Attribute errors occur when you try to access a property (or method) that does not exist in an object. For example:

class Person:
    pass

person = Person()
person.age
Enter fullscreen mode Exit fullscreen mode
Traceback (most recent call last):
  ...
AttributeError: 'Person' object has no attribute 'age'
Enter fullscreen mode Exit fullscreen mode

Another example

# Empty dictionary
records = {}

# Works! items() method exist
records.items()

# Throws as AttributeError exception
records.age()
Enter fullscreen mode Exit fullscreen mode
Traceback (most recent call last):
  ...
AttributeError: 'dict' object has no attribute 'age'
Enter fullscreen mode Exit fullscreen mode

Ensure you are using attributes that exist to avoid this error.

ImportError

An import error usually occurs when you try to import a non-existent variable, function, or module from another module. For example, try to import Fish from the abstract base class (abc) module:

from abc import Fish
Enter fullscreen mode Exit fullscreen mode
...
ImportError: cannot import name 'Fish' from 'abc'
...
Enter fullscreen mode Exit fullscreen mode

ModuleNotFoundError

A ModuleNotFoundError is a subclass of ImportError. When a module does not exist or is not reachable, Python raises a ModuleNotFoundError. For example, I don't have a module called sky:

import sky
Enter fullscreen mode Exit fullscreen mode
...
ModuleNotFoundError: No module named 'sky'
Enter fullscreen mode Exit fullscreen mode

The solution is to ensure that a file sky.py exists within your project directory.

IndexError

An IndexError is raised when you supply a sequence (e.g. list, tuple, etc.) with an index that is out of range. Slice indices are silently truncated to fall in the allowed range. For example:

favorites = ['Peter', 'Stewie', 'Brian', 'Quagmire', 'Joe']

# These works
print(favorites[1]) # Stewie
print(favorites[1:2000])
print(favorites[1000:2000]) # []

# This fails: index is from 0 - 4
print(favorites[10])
Enter fullscreen mode Exit fullscreen mode
Stewie
['Stewie', 'Brian', 'Quagmire', 'Joe']
[]

Traceback (most recent call last):
...
IndexError: list index out of range
Enter fullscreen mode Exit fullscreen mode

KeyError

A KeyError occurs when you try to access a dictionary key that does not exist. For example:

oldest_person = { 'name': 'Methuselah Doe', 'age':  1000 }

# Ok!
print(oldest_person['name'])

# Oops!
print(oldest_person['occupation'])
Enter fullscreen mode Exit fullscreen mode
Traceback (most recent call last):
...
KeyError: 'occupation'
Enter fullscreen mode Exit fullscreen mode

To avoid KeyError, use the dictionary's .get() method. get() returns None for a non-existent key. For example:

print(oldest_person.get('occupation')) # None
Enter fullscreen mode Exit fullscreen mode

Also, .get() optionally takes a default value that gets returned when the key does not exist. For example:

print(oldest_person.get('occupation', 'Musician')) # Musician
Enter fullscreen mode Exit fullscreen mode

KeyboardInterrupt

When you hit the interrupt key (Control-C or Delete), Python raises a keyboardinterrupt exception. For example:

# Simulate a long-running program
import time

while True:
    print(time.time())

    # add some delay
    time.sleep(1)
Enter fullscreen mode Exit fullscreen mode
1707814961.0006871
1707814962.0126708
1707814963.0137038
1707814964.014804
Traceback (most recent call last):
  ...
KeyboardInterrupt
^C
Enter fullscreen mode Exit fullscreen mode

NameError

Python raises a NameError when you use a global or local name or variable (defined within a function) that you have not created. For example:

print(favorite_food)
Enter fullscreen mode Exit fullscreen mode
Traceback (most recent call last):
...
NameError: name 'favorite_food' is not defined
Enter fullscreen mode Exit fullscreen mode

The solution is to ensure you define a variable before using it. For example:

favorite_food = "Whatever works!"

# Works!
print(favorite_food) 
Enter fullscreen mode Exit fullscreen mode

IndentationError

An indentation error is a syntax error that occurs when your code is not properly indented. Thankfully, most code editors will help you avoid this kind of error. In the example below, I inserted two spaces before calling print()

  print('Hello')

# IndentationError: unexpected indent
Enter fullscreen mode Exit fullscreen mode

TypeError

A TypeError occurs when you perform an operation or apply a function to an object of inappropriate type. For example, summing two dictionaries or converting an inappropriate string to an integer will raise a TypeError.

class Person:
    pass

# Fails!
map(lambda x: x * 2, Person())
# TypeError: 'Person' object is not iterable

# Fails!
print({} + {})
# TypeError: unsupported operand type(s) for +: 'dict' and 'dict'
Enter fullscreen mode Exit fullscreen mode

ValueError

A ValueError exception occurs when an operation or function receives an argument with the right type but an inappropriate value. A classic example is when you try to convert an inappropriate string to an integer.

print(int('abc'))

# ValueError: invalid literal for int() with base 10: 'abc'
Enter fullscreen mode Exit fullscreen mode

ZeroDivisionError

A ZeroDivisionError exception occurs when you attempt to divide (or modulo) a value by zero. For example:

print(4 / 0)
# ZeroDivisionError: division by zero

print(3 % 0)
# ZeroDivisionError: integer modulo by zero
Enter fullscreen mode Exit fullscreen mode

The base class of ZeroDivisionError is the ArithmeticError. The ArithmeticError is also the parent class of OverflowError, an exception raised when the result of an arithmetic operation is too large to be represented.

File Errors

Other exceptions you might encounter include:

  • FileExistsError: Occurs when trying to create a file or directory which already exists
  • FileNotFoundError: Raised when a file or directory is requested but doesn’t exist.
with open('hello.txt', mode='rt', encoding='utf-8') as f:
    print(f.read())

# Side note: The program never gets here
print('I will never get here if hello.txt does not exist')
Enter fullscreen mode Exit fullscreen mode
...
FileNotFoundError: [Errno 2] No such file or directory: 'hello.txt'
Enter fullscreen mode Exit fullscreen mode

Do check the Python documentation for more types of exceptions.

Handling Exceptions

To prevent your program from crashing you need to handle possible exceptions. In Python, you use the try-except statement to handle exceptions. A classic example is asking for a user's age until he provides the expected value:

while True:
    try:
        age = int(input("Please enter your age: "))
        if age >= 18:
            break
        else:
            print("The age you provided is less than 18!")
    except ValueError:
        print("Oops!  That was no valid number.  Try again...")

Enter fullscreen mode Exit fullscreen mode
Please enter your age: 12
The age you provided is less than 18!
Please enter your age: weerr
Oops!  That was no valid number.  Try again...
Please enter your age: 45
Enter fullscreen mode Exit fullscreen mode

Let's handle the FileNotFoundError you saw earlier.

try:
    with open('hello.txt', mode='rt', encoding='utf-8') as f:
        print(f.read())
except FileNotFoundError as e:
    print(e)

# Our program continues because we handled the exception above
print('I will run even if hello.txt does not exist')
print('Me too!')
Enter fullscreen mode Exit fullscreen mode
[Errno 2] No such file or directory: 'hello.txt'
I will run even if hello.txt does not exist
Me too!
Enter fullscreen mode Exit fullscreen mode

A try statement can have multiple except blocks or clauses. Also, an except clause may have many exceptions in a tuple with parenthesis. Here is a contrived example modified from Python documentation:

def divide(a, b):
    return a / b


try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
    divide(10, 0)
except OSError as err:
    print("OS error:", err)
except ValueError:
    print("Could not convert data to an integer.")
except ZeroDivisionError as err:
    print("Zero division error:", err)
except Exception as err:
    print(f"Unexpected {err=}, {type(err)=}")
Enter fullscreen mode Exit fullscreen mode

You can shorten the code above to

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
    divide(10, 0)
except (OSError, ValueError, ZeroDivisionError) as err:
    print(err)
except Exception as err:
    print(f"Unexpected {err=}, {type(err)=}")
Enter fullscreen mode Exit fullscreen mode
  • A call to open() might raise a FileNotFoundError or any other exception that is a subclass of OSError, so we use OSError as a catch-all for these exception types.
  • Converting an invalid string to an integer will raise a ValueError. We handle that directly.
  • A call to divide(10, 0) raises a ZeroDivisionError exception. We handle the exception. A try-except statement can handle any exception raised from a function.
  • All built-in, non-system-exiting exceptions are subclasses of the Exception class, so we handle any other form of exception that might occur. The Exception class derives from the BaseException class, the mother of all exception classes.

Consider using specific exception types such as FileNotFoundError before using the general ones like OSError.

Including an Else Clause

You can add an optional else clause to the try-except statement. The else clause must follow all except clauses. Use it for code that must run if the try clause does not raise an exception. For example:

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except OSError:
        print('cannot open', arg)
    else:
        print(arg, 'has', len(f.readlines()), 'lines')
        f.close()
Enter fullscreen mode Exit fullscreen mode

Using the else clause is better than adding additional code to the try clause because it avoids accidentally catching an exception from another code you did not intend to protect with the try-except statement.

Adding a Finally Clause

Also, you can include an optional finally block if you need to run some form of "clean up" code. Unlike the else clause, the code in the finally block will always run regardless of an exception. For example:

def divide(x, y):
    try:
        result = x / y
    except TypeError:
        print(f"divide({x}, {y}): You cannot divide {x} by {y}")
    except ZeroDivisionError:
        print(f"divide({x}, {y}): You cannot divide {x} by zero.")
    else:
        # Only runs if the try block succeeds
        print(f"divide({x}, {y}): result is {result}")
    finally:
        # Always runs
        print("executing finally clause")
Enter fullscreen mode Exit fullscreen mode
  • Dividing with unsupported types such as strings will raise a TypeError which is handled in the first except block. The finally block runs. The program ends.
  • The second except block catches a ZeroDivisionError. The finally block runs. The program ends.
  • The else clause runs when divide() succeeds. The finally block runs. The program ends.
divide('hello', 'world')
print() # for space
divide(4, 0)
print() # for space
divide(3, 2)
Enter fullscreen mode Exit fullscreen mode
divide(45, 2): You cannot divide hello by world
executing finally clause

divide(4, 0): You cannot divide 4 by zero.
executing finally clause

divide(3, 2): result is 1.5
executing finally clause
Enter fullscreen mode Exit fullscreen mode

Raising Exceptions

There are instances where you want to raise exceptions if a user's input does not meet the condition you have specified. You can do so using the raise statement. For example:

def divide_only_ints(num1: int, num2: int):
    if isinstance(num1, int) and isinstance(num2, int):
        print(f'num1 / num2 = {num1 / num2 :.2f}')
    else:
        raise TypeError(f'num1 and num2 must be ints!\nGot {num1} and {num2} instead')

# Works!
divide_only_ints(4, 3) # num1 / num2 = 1.33

# Fails
divide_only_ints(4.5, 12.3)
Enter fullscreen mode Exit fullscreen mode
Traceback (most recent call last):
...
TypeError: num1 and num2 must be ints!
Got 4.5 and 12.3 instead
Enter fullscreen mode Exit fullscreen mode

raise Exception is the same as raise Exception(). You can optionally include a custom message. For example, raise Exception('My custom message').

With the raise keyword, you can re-raise or propagate an exception if you don't want to handle it. For example:

try:
    divide_only_ints("a", "b")
except TypeError:
    print('An exception occurred! Sending it your way!')
    raise
Enter fullscreen mode Exit fullscreen mode
An exception occurred! Sending it your way!
...
TypeError: num1 and num2 must be ints!
Got a and b instead
Enter fullscreen mode Exit fullscreen mode

You can also propagate multiple exceptions from an except block using a raise or raise ... from statement. This propagation is known as exception chaining. For example:

try:
    divide_only_ints("a", "b")
except TypeError:
    raise ValueError('Just because I can')
Enter fullscreen mode Exit fullscreen mode

Or using raise ... from:

try:
    divide_only_ints("a", "b")
except TypeError as err:
    raise ValueError('Just because I can') from err
Enter fullscreen mode Exit fullscreen mode
Traceback (most recent call last):
  ...
TypeError: num1 and num2 must be ints!
Got a and b instead

During the handling of the above exception, another exception occurred:

Traceback (most recent call last):
  ...
ValueError: Just because I can
Enter fullscreen mode Exit fullscreen mode

Adding More Information to Exceptions

The section requires Python >= 3.11

Sometimes, you want to add more information to an exception before re-raising it. You can use the exception's .add_notes() method for such. For example:

try:
    divide_only_ints("a", "b")
except TypeError as err:
    err.add_note('I used the divide_only_ints() function')
    err.add_note('The left operand was the letter a')
    err.add_note('The right operand was the letter b')
    print('An exception occurred! So Sending it your way!')
    raise
Enter fullscreen mode Exit fullscreen mode
An exception occurred! So Sending it your way!
Traceback (most recent call last):
  ...
TypeError: num1 and num2 must be ints!
Got a and b instead
I used the divide_only_ints() function
The left operand was the letter a
The right operand was the letter b
Enter fullscreen mode Exit fullscreen mode

User-Defined Exceptions

Finally, you can define your exceptions by deriving from the Exception class directly or indirectly. For example:

class DivideByIntOnlyError(Exception):
    """An example of a user-defined exception."""

    def __init__(self, *args: object) -> None:
        super().__init__(*args)

    def add_note(self, __note: str) -> None:
        print('About to start adding notes for useless logging purposes')
        return super().add_note(__note)


def divide_only_ints(num1: int, num2: int):
    if isinstance(num1, int) and isinstance(num2, int):
        print(f'num1 / num2 = {num1 / num2 :.2f}')
    else:
        raise DivideByIntOnlyError(
            f'num1 and num2 must be ints!\nGot {num1} and {num2} instead')
Enter fullscreen mode Exit fullscreen mode
try:
    divide_only_ints("a", "b")
except DivideByIntOnlyError as err:
    err.add_note('I used the divide_only_ints() function')
    err.add_note('The left operand was the letter a')
    err.add_note('The right operand was the letter b')
    print('An exception occurred! So Sending it your way!')
    raise
Enter fullscreen mode Exit fullscreen mode
About to start adding notes for useless logging purposes
About to start adding notes for useless logging purposes
About to start adding notes for useless logging purposes
An exception occurred! So Sending it your way!
Traceback (most recent call last):
  ...
DivideByIntOnlyError: num1 and num2 must be ints!
Got a and b instead
I used the divide_only_ints() function
The left operand was the letter a
The right operand was the letter b
Enter fullscreen mode Exit fullscreen mode

Note that overriding the __init__() and add_note() methods within DivideByIntOnlyError is unnecessary in most cases. I've included it here for demo purposes only.
Also note that While you can define Exception classes to do anything any other class can, it is best to keep it simple.

Summary

In this article, you saw examples of exceptions like IndexError and KeyError and how they may occur. You also saw how to handle exceptions using the try-except statement with or without else or finally. Next, you learned how to raise exceptions, chain them, and add more details to the exceptions. Lastly, you learned how to define your exception by subclassing the Exception class.

Thank you for reading.

Top comments (0)