Exception and File Handling in Python

Explore the intricacies of exception and file handling, why they are important, and how to implement them effectively in Python.

Exception and File Handling in Python

As a Python developer, you will frequently encounter errors, both those that are expected and those resulting from human input. Additionally, you will often need to read from and write to files. Exception handling and file handling are fundamental concepts of Python programming, and a thorough understanding of these concepts, including when to apply them and how to implement them correctly, is essential for enhancing your development skills and ensuring the reliability and functionality of the solutions and software you develop.

In this article, we’ll explore the intricacies of both these topics, why they are important, and how to implement them effectively in Python. Let’s explore each topic in depth.

Prerequisites:

  • Installed Python: Familiarity with running Python scripts.
  • Python Basics: Prior knowledge of the basics of Python programming including control flow, functions, data types, etc.

If you’re not familiar with Python or need a comprehensive introduction to the language, I recommend visiting the official Python documentation’s tutorial section.

Exceptions

Errors detected during execution are called exceptions they are a result of both known errors and human errors*.* There are times when a statement or expression throws an error when executing even if the syntax of the statement is technically correct. Exceptions are used to handle errors and exceptional instances.

>>> 10 / 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str

Types of Exception/ Errors in Python

The base exception class is BaseException, and it has various subclasses representing different types of exceptions.

Here are a few common exception types:

  1. ZeroDivisionError: If you try to divide a number by zero, it will result in a ZeroDivisionError exception, even though the division operation itself is syntactically valid. The reason is because being that a number cannot be divided by zero, it is mathematically incorrect. The error message would look like this:
>>> 10 / 0
ZeroDivisionError: division by zero

2. NameError: When you attempt to access a variable that hasn’t been defined locally or globally, NameError is raised, even if your code structure is correct. It is very common to use the wrong spelling of a variable or use a very you haven’t declared, you have to ensure that you have defined the variable you use. The error message would look like this:

>>> print(variable_name)
NameError: name 'variable_name' is not defined

3. TypeError: When you attempt an operation or function that is not supported for a particular data type, TypeError is raised. You may want to perform some action without fully understanding what is attemplable to that specific data type. Some examples of TypeError are:

>>> result = "hello" + 5
TypeError: can only concatenate str (not 'int') to str
>>> my_list = [1, 2, 3]
    my_list.append("4")
TypeError: unsupported operand type(s) for +: 'int' and 'str'>>> my_list = [1, 2, 3]
    index = my_list["2"]
TypeError: list indices must be integers or slices, not str>>> list1 = [1, 2, 3]
    result = list1 + "four"
TypeError: can only concatenate list (not "str") to list>>> def multiply(a, b):
      return a * b
    result = multiply(5)
TypeError: multiply() missing 1 required positional argument: 'b'

4. FileNotFoundError: it is raised when a file or directory is requested but doesn’t exist. When working with files, opening a file that doesn’t exist or attempting to read from a file that you don’t have permission to access raises FileNotFoundError. The error message would look like this:

>>> with open("does_not_existent_file.txt", "r") as file:
      	content = file.read()
FileNotFoundError: [Errno 2] No such file or directory: 'does_not_existent_file.txt'

5. IndexError: When a sequence subscript is out of range, IndexError is raised. It occurs when you try to access an element in a sequence (like a list or a string) using an index that is outside the valid range of indices for that sequence.

>>> my_list = [1, 2, 3]
    value = my_list[5]  # Accessing an index that doesn't exist out of range
IndexError: list index out of range

6. KeyError: Dictionaries are collections of key-value pairs, and when you attempt to access a key that is not present in the dictionary, Python raises a KeyError.

>>> my_dict = {"name": "Victory", "age": 19}
    print(my_dict["city"]) # Attempting to access a key 'city' that is not defined in the dictionary
KeyError: 'city'

7. ValueError: When an operation or function receives an argument that has the right type but an inappropriate or invalid value. Most often, it occurs when parsing user input or external data, and you need to ensure that the input is of the expected format before performing operations on it.

>>> int("abc") # Attempting to convert a non-integer string to an integer
ValueError: invalid literal for int() with base 10: 'abc'

8. ModuleNotFoundError: When you attempt to import a Python module that cannot be found, it raises ModuleNotFoundError. It often occurs when there’s a misspelled module name/ file name or the module doesn’t exist at all. Here’s an example:

>>> import non_existent_module
ModuleNotFoundError: No module named 'not_found_module'

9. PermissionError: When we try to run an operation without adequate access rights (filesystem permissions), PermissionError is raised. Handling PermissionError is very important when working with files, especially in cases where you need to ensure that your program can deal with permission-related errors.

>>> with open("some_protected_file.txt", "w") as file:
        file.write("This is a protected file.")  # Writing to a read-only file
PermissionError: [Errno 13] Permission denied: 'some_protected_file.txt'

Python provides a wide range of built-in exceptions to cover various error scenarios. You can explore and learn about these exceptions in more detail in the official Python documentation.

Built-in Exceptions

Handling Exception

Exception handling is a way to deal with errors or exceptions that may occur during the execution of a Python program. It helps prevent the program from crashing when unexpected errors occur.

Exception Handling in Python:

Exception Handling in Python

Key Exception components

Python provides several elements for effective exception handling:

  • Try: The try block contains the code that is to be executed and might raise an exception. Python attempts to execute this code. The statement(s) between the try and except keywords is called the try clause.
  • Except: If an exception occurs during the execution of the try clause, the rest of the clause is skipped. The except block specifies code to the executed if exceptions occur within the try block.
try:
    x = int(input("Please enter a number: "))
except Exception as err:
    print("invalid number.")

You can specify what should be done for different types of exceptions. You can have multiple except blocks. If its type matches the exception named after the except keyword, the except clause is executed, and then execution continues after the try/except block. You can also handle unexpected errors by assigning the error to a variable. Exception to a variable, and then the variable can be accessed.

try:
    num1 = int(input("Enter the first number: "))
    num2 = int(input("Enter the second number: "))
 result = num1 / num2

except ValueError:
    print("Invalid input. Please enter valid integers.")
except ZeroDivisionError:
    print("Division by zero is not allowed.")
except Exception as err:
    print("Unexpected Error", err)
  • else: This contains a block of code that is executed If no exceptions occur within the try block.
  • Finally: This contains code that executes regardless of whether an exception is raised or not. It’s used for cleanup operations.
try:
    num1 = int(input("Enter the first number: "))
    num2 = int(input("Enter the second number: "))
 result = num1 / num2

except ValueError:
    print("Invalid input. Please enter valid integers.")
except ZeroDivisionError:
    print("Division by zero is not allowed.")
else:
    print("Result of division:", result)
finally:
    print("Execution complete.")

Raising Exceptions

The raise statement allows the programmer to force a specified exception to occur. raise statement is used to indicate that an error or exceptional condition has occurred, allowing you to easily handle it.

Here’s the basic syntax of the raise statement:

raise ExceptionType("Error message")
  • ExceptionType is the type of exception you want to raise. It can be a built-in exception type like ValueError, TypeError, or a custom exception type you’ve defined.
  • “Error message” is an optional argument that allows you to provide a custom error message that describes the reason for raising the exception.

Raising a Built-in Exception

You can raise any of the built-in exception types like NameError, ValueError, IndexError, and many more.

def divide(x, y):
    if y == 0:
        raise ZeroDivisionError("Division by zero is not allowed.")
    return x / y
try:
    result = divide(10, 0)
except ZeroDivisionError as e:
    print(f"Error: {e}")

Raising a Custom Exception

In the course of development, you might want to create define, and raise exceptions that are specific to your application or module. To create a custom exception, you define your own exception class to inherit from one of the built-in exception classes, such as Exception, BaseException, or one of their subclasses. This allows you to that are specific to your application or module.

Here’s how you can define a custom exception:

class MyCustomError(Exception):
    def __init__(self, message):
        super().__init__(message)

Here’s how you raise your custom exception:

class MyCustomError(Exception):
    def __init__(self, message):
        self.message = message
def process_data(data):
    if not data:
        raise MyCustomError("Data is empty, cannot process.")try:
    data = ""
    process_data(data)
except MyCustomError as e:
    print(f"Custom Error: {e}")

How to implement exceptions with examples

Take a look at this example from Python documentation:

import sys
try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("OS error:", err)
except ValueError:
    print("Could not convert data to an integer.")
except Exception as err:
    print(f"Unexpected {err=}, {type(err)=}")
    raise

Here’s an example that demonstrates the use of try, except, else, and finally blocks in Python:

try:
    num1 = int(input("Enter the first number: "))
    num2 = int(input("Enter the second number: "))
    sum = numerator + denominator
except ValueError:
    print("Please enter valid integers.")
except TypeError:
    print("Please enter valid integers.")
else:
    print("Total is:", s)
finally:
    print("Solved!")

Here’s another example that demonstrates the use of try, except, else, and finally blocks in Python, but this time with a different scenario involving division by zero:

try:
    numerator = 10
    denominator = 0
    result = numerator / denominator
except ZeroDivisionError:
    print("Division by zero is not allowed.")
else:
    print("Result:", result)
finally:
    print("This block always executes, whether an exception occurred or not.")

Exception Handling Best Practices

1. Use specific exception types: Instead of catching general exceptions like Exception or BaseException, catch specific exceptions that accurately represent the error. This makes your code more readable and helps you handle different types of exceptions differently.

try:
    # Some code that may raise a specific exception
except SpecificException as e:
    # Handle the specific exception

2. Avoid Bare except Clauses: Don’t use bare except clauses (without specifying an exception type) as they catch all exceptions, including those that may indicate severe errors.

3. Keep Exception Blocks Short: Avoid wrapping large portions of code in exception handlers unnecessarily in try and except blocks, as it can make your code harder to read and maintain.

4. Use finally for Cleanup: Always Use the finally block for cleanup code that should always execute, whether or not an exception occurs.

try:
    # Some code that may raise an exception
except SpecificException as e:
    # Handle the exception
finally:
    # Code that always runs (e.g., cleanup)

5. Custom Exceptions: Create custom exception classes when your application has specific error conditions, it improves code readability, and allows you to handle exceptional cases more effectively.


File handling

Reading and writing data to files; is an operation that is common in many applications. It is important to fully understand how to work with files, so has to make your applications/ software dynamic. Python provides built-in functions and methods for working with files.

Input and Output in Python (I/O)

It is essential to understand I/O as it allows your programs to communicate with users, read data from external sources, and write data to files.

Input (Reading Data)

  • Reading from the Keyboard (Standard Input): Your program might need to get data from the users. You can read user input using the input() function.
user_input = input("Enter your name: ")

There are other ways of reading data open(), read() and readlines(). I’ll explain them in the next section.

Output (Writing Data)

  • Writing to the Console (Standard Output): You can write using the print() function.
print("Hello, World!")

There are other ways of reading data open() and read(). I’ll explain them in the next section.

Types of mode

When working with file I/O in Python, you can specify various file modes that determine how the file should be opened and used. They are:

  1. Read Mode (‘r’): It allows to read the contents of the file. It is the default mode when you open a file using the open() function. If the file does not exist, it will raise a FileNotFoundError.
  • Example:
f = open("file.txt", "r", encoding="utf-8")
  1. Write Mode (‘w’): This mode allows you to write data to a file. It overwrite the existing file that is, if the file already exists, its contents will be deleted then it writes the new data into it. If the file does not exist, a new one will be created.
  • Example:
with open("example.txt", "a") as file:
    file.write("This is an example\n")
  1. Append Mode (‘a’): Like write mode, it is used to write data to a file, the difference is that it is added to the end of the existing file. If the file does not exist, a new one will be created.

f = open('file.txt', 'a', encoding="utf-8")

  1. Read and Write mode (‘r+’): It is used to open a file for both reading and writing. It means that you can both read from and write to the file within the same file object. If the file does not exist, it will raise a FileNotFoundError.
# Opening a file in 'r+' mode
with open("example.txt", "r+") as file:
    content = file.read()  # Reading from the file
    print("Read:", content)
 # Writing to the file
    file.write("Appending new data.")
    file.seek(0)  # Move the file cursor back to the beginning
    content = file.read()  # Read the updated content
    print("Updated Content:", content)
  1. Binary Mode (‘b’): Binary mode is used with the read or write modes to indicate that the file should be treated as binary rather than text.
  • ‘wb’
  • ‘rb’
  • ‘ab’
  • ‘rb+’

Key file handling functions and methods

  • open(): The open() function is used to open files in various modes (e.g., read, write, append). It is used with two positional arguments and one keyword argument: open(filename, mode, encoding=None), It returns a file object that can be used for a variety of operations including reading, writing, and manipulating the contents of the file. If encoding is not specified, UTF-8 is the default encoding (modern de-facto standard).
  • close(): The close() function is used to close a file after all operations are complete. It is very important to call the close() function to close the file and immediately free up any system resources used by it.
f = open('output.txt', 'w', encoding="utf-8")
data = f.read()
print(data)
f.close()

Note: If you open() a file using the with keyword then you don’t have to close it, because it automatically closes the file.

After the with statement or close() function, you won’t be able to access the file object for that instance.

>>> f.close()
>>> f.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file
  • Read functions: They are readline(), readlines(), and read():

readline(): The readline() function reads a single line from the file and returns it as a string. Anewline character (\n) is left at the end of the string,

with open("example.txt", "r") as file:
    line = file.readline()

readlines(): The readlines() function reads all the lines of a file and returns them as a list of strings. list(file_object) can also be used to read files into the list. Each element in the list represents a line from the file.

>>> with open("example.txt", "r") as f:
...    f.readlines()
['This is the first line of the file.\n', 'Second line of the file\n']
>>>
>>> with open("example.txt", "r") as f
...    list(f)
['This is the first line of the file.\n', 'Second line of the file\n']

read(): The read() function reads the entire content of the file and returns it as a single string. You can read the entire file or a specific number of characters. The syntax is read(size). size is an optional argument, if specified it returns the size number of characters.

# Opening a file in read mode
>>> with open("example.txt", "r") as file:
...    text_25 = file.read(25) # returns only 25 characters from the file
...    data = file.read() # returns the entire file content as a string
...    print(data)

Note: you can also loop through the file object to print/access each line in the file:

# Opening a file in read mode
with open("example.txt", "r") as file:
   for line in f:
       print(line, end='')
  • Write functions: write() and writelines():

writelines(): The writelines() function is used to write a list of strings to a file. Each element in the list represents a line to be written to the file. It does not return any value; it returns None.

# Opening a file in write mode
with open("output.txt", "w") as file:
    file.writelines(["Line 1\n", "Line 2\n", "Line 3\n"])

— write(): The write() function is used to write data to a file that has been opened in write (‘w’) or append (‘a’) mode. It can be used to create new files, overwrite existing ones (‘w’), or append to an existing file (‘a’).
The only argument it takes is the data you want to write which is a usually a string.

It returns the number of characters written.

# Opening a file in write mode
with open("output.txt", "w") as file:
    file.write("This is a sample text.")
# Appending to a file
with open("output.txt", "a") as file:
    file.write("\nAppending more text.")>>> f.write('This is a test\n')
15

Note: you can also loop a list and write data to a file line by line, just like reading.

lines = ["Line 1", "Line 2", "Line 3"]
with open("output.txt", "w") as file:
    for line in lines:
        file.write(line + "\n")

— seek(offset, whence): The seek() method is used to change the current file position (cursor) within an open file. The two arguments it takes

  • offset: This specifies the new file position, relative to a reference point determined by the whence argument.
  • whence: This specifies the reference point for the offset and can take one of three values:
    • 0: Seek from the beginning of the file. (0 is the default reference point if not specified)
    • 1: Seek from the current position.
    • 2: Seek from the end of the file.

tell(): The tell() method is used to retrieve the current file position (cursor) within an open file. It returns an integer representing the current file position, measured in bytes from the beginning of the file. It does not take any argument.

# Open a file for reading
file = open("example.txt", "r")
# Read the first 10 bytes
data = file.read(10)

# Get the current file position (cursor) using tell()
position = file.tell()

print("Data:", data)
print("Current position:", position)

# Move the cursor to the beginning of the file using seek()
file.seek(0)

# Read the entire content from the beginning
content = file.read()
print("\nFull Content:\n", content)

# Close the file
file.close()
>>> file = open("example.txt", "r")
>>> file.seek(20) # Move the cursor to the 20th byte from the beginning
>>> content = file.read() # Read and print the content from the current position
>>> print("Content from position 20:\n", content)
>>> file.close()

JSON

Reading and Writing JSON Data

Python’s json module allows you to read and write JSON data easily.

Reading JSON Data

To read JSON data from a file or a JSON string, you can use the json.load(). load() is used to decode the file object; read and parse the JSON data from the file. It returns either dictionary or a list representing the JSON data. Here’s an example of how to read JSON data from a file:

import json
with open("data.json", "r") as file:
    data = json.load(file)
print(data)

To parse a JSON string into a dictionary, you can use the json.loads() method:

import json
json_string = '{"name": "Victory", "age": 12, "city": "Lagos"}'
data = json.loads(json_string) # 'data' now contains the parsed JSON data as a Python dictionary
print(data)

Writing JSON Data

import json
data = {"name": "Alice", "age": 30}
with open("data.json", "w") as file:
    json.dump(data, file)

File Handling Best Practices

  • Always remember to handle exceptions, especially when working with files, to be able to control the error that the application might face. Implement error handling using try, except, else, and finally blocks.
  • Make sure you close() files explicitly (When Not Using with):
  • It is good practice to use the with keyword when dealing with file objects. The advantage is that the file is properly closed after its suite finishes, even if an exception is raised at some point.
# Opening a file in read mode
>>> with open("example.txt", "r") as file:
...    data = file.read()
...    print(data)
>>> f.closed  # with automatically closes the file object after the statement
True
  • When opening files, specify the file mode (‘r’ for read, ‘w’ for write, ‘a’ for append, ‘b’ for binary, etc.). Avoid relying on the default mode, as it can vary depending on the Python environment.
  • Be explicit when specifying file paths. Use relative paths for files within your project directory and absolute paths for files outside of it. Avoid hardcoding paths whenever possible.
  • Before opening a file, check if it exists using functions like os.path.exists() or Path.exists() (from the pathlib module) to avoid unnecessary exceptions.
  • Before attempting to open or modify a file, check whether your program has the necessary permissions to access the file. Handle permission-related errors appropriately.
  • When working with non-text files, such as images or executables, open them in binary mode (‘rb’ for reading, ‘wb’ for writing) to prevent any unintended encoding/decoding of data.

Resources

Python documentation:

3.11.5 Documentation

Conclusion

In conclusion, both exception handling and file manipulation play pivotal roles in Python programming. They equip developers with the ability to manage errors effectively and manipulate data efficiently, establishing Python as a versatile choice for a diverse array of applications.

That concludes our comprehensive exploration of exception and file handling, encompassing their usage guidelines and best practices, ensuring the reliability and effectiveness of your code.

Thanks for reading!

Enjoyed this article?

Share it with your network to help others discover it

Continue Learning

Discover more articles on similar topics