Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 3

Handling File, Network, and Data Errors with Targeted Exceptions

Recommended resources

Welcome to the third lesson in our journey to build reliable automation scripts. In the last session, you learned how to point your scripts to specific files and how to read and write their contents, including using JSON for structured data. We even built a simple workflow-state record to help a script remember what it did last.

But what happens when things don't go as planned? A script might try to read a file that's been moved, parse a JSON file that's malformed, or connect to a network resource that's offline. Without a strategy to handle these situations, your script will crash, often with a long, unhelpful error message. This lesson addresses that exact problem. You will learn how to handle expected file, network, and data errors using Python's try...except blocks, a fundamental skill for moving from simple scripts to robust, production-ready automations.

Understanding Exceptions

When an error occurs during the execution of a Python script, it's called an exception. If your code doesn't explicitly handle the exception, the program stops and prints a "traceback"—a detailed report of where the error happened. While useful for you as a developer, this is not a graceful way for a program to fail.

The official Python documentation provides a clear distinction between syntax errors (which prevent the program from running at all) and exceptions (which occur during execution).

8. Errors and Exceptions — Python 3.12.13 documentation

Start by reading the introduction and the short section on "Exceptions".

Please read from this section to understand what exceptions are and see examples of common ones like ZeroDivisionError, NameError, and TypeError.


The try...except Block: Your Safety Net

To prevent your script from crashing, you can wrap potentially problematic code in a try...except block. This is the core mechanism for error handling in Python.

The structure is straightforward:

  1. The code that might cause an exception goes inside the try block.
  2. If an exception occurs in the try block, Python immediately jumps to the except block.
  3. If no exception occurs, the except block is skipped entirely.

The following video from Corey Schafer gives an excellent visual introduction to this concept, using the very practical example of a missing file.

Python Tutorial: Using Try/Except Blocks for Error Handling

In this video, "Python Tutorial: Using Try/Except Blocks for Error Handling," we'll see the basic structure in action.

Watch the segment from the beginning where he demonstrates what happens with an unhandled FileNotFoundError and then shows how a try...except block provides a much cleaner way to handle it.


Be Specific: Catching the Right Errors

A common mistake is to catch every possible exception with a generic except Exception:. This is dangerous because it can hide real bugs in your code. The goal is not to ignore all errors, but to gracefully handle the ones you expect to happen.

For example, when reading a file, you might expect a FileNotFoundError, but you probably don't expect a NameError caused by a typo in your code. By catching only FileNotFoundError, you handle the anticipated issue without masking the unexpected one.

Corey Schafer's video explains this critical point very well.

Python Tutorial: Using Try/Except Blocks for Error Handling

Let's continue with the same video to understand the importance of specificity.

Watch the segment from this point. He shows how a generic except block can hide a NameError and then demonstrates the correct way to catch multiple, specific exceptions. Pay attention to how he uses except FileNotFoundError as e: to get access to the error message itself.


Handling Common Automation Errors

Your automation scripts will frequently encounter three categories of errors: file system errors, data errors, and network errors. Let's see how to handle each one.

1. File System Errors

When working with files, several things can go wrong. The file might not exist, or your script might not have permission to read or write it. The OpenSSF Secure Coding Guide provides a handy list of these common OS-related exceptions.

pyscg-0015: Handle Error Conditions | OpenSSF Best Practices Working Group

This guide offers best practices for writing secure and reliable code. We'll focus on its advice for file operations.

Take a look at the table of exceptions. You'll see FileNotFoundError and PermissionError, two of the most common errors you'll need to handle in file automation.

Now, let's apply this. In the previous lesson, we used pathlib to read files. Here’s how you can make that code more robust by handling potential errors.

import json
from pathlib import Path

# A path to a file we expect to be there.
# Try changing this to a non-existent file or a file you don't have read permissions for.
config_path = Path("config.json") 

print(f"Attempting to read configuration from: {config_path}")

try:
    # The operation we are "trying"
    config_text = config_path.read_text(encoding="utf-8")
    config_data = json.loads(config_text)
    print("Configuration loaded successfully!")
    # We could do more with config_data here...

except FileNotFoundError:
    # This block runs ONLY if the file doesn't exist.
    print(f"Error: Configuration file not found at '{config_path}'.")
    print("Please make sure the file exists.")
    
except PermissionError:
    # This block runs ONLY if we don't have permission to read the file.
    print(f"Error: Insufficient permissions to read '{config_path}'.")
    
except json.JSONDecodeError as e:
    # This block runs if the file is not valid JSON.
    print(f"Error: The configuration file is corrupted or not valid JSON.")
    print(f"Details: {e}")

except Exception as e:
    # A final catch-all for any other unexpected errors.
    print(f"An unexpected error occurred: {e}")

This pattern of stacking specific except blocks, from most specific to most general, is a best practice for writing resilient code.

2. Network Errors

Automations often need to interact with web services. The requests library is the standard for this in Python. When you make a web request, the server might be down (ConnectionError) or it might respond with an error code like "404 Not Found" (HTTPError). The principles of exception handling are the same.

This video from John Watson Rooney provides a concise and practical guide to handling these specific network errors.

How To Handle Errors & Exceptions with Requests and Python

This video, "How To Handle Errors & Exceptions with Requests and Python," is a perfect walkthrough for the "network errors" part of our learning goal.

Watch from the beginning. The video covers: The two main types of errors: ConnectionError and HTTPError. Using response.raise_for_status() to turn bad status codes (like 404 or 500) into an exception. Wrapping the request in a try...except block to catch HTTPError and ConnectionError. Using the general requests.exceptions.RequestException to catch any request-related error.


Advanced Control Flow: else and finally

The try...except structure has two more optional clauses: else and finally.

  • else: The code in the else block is executed only if the try block completes without raising an exception. This is useful for separating your "success" logic from the code you are monitoring for errors.
  • finally: The code in the finally block is executed no matter what—whether the try block succeeded, an exception was caught, or an unexpected exception occurred. This is essential for cleanup actions, like closing a database connection or releasing a resource.

Let's consult the official Python documentation for the formal definition, and then Corey Schafer's video for a practical demonstration.

8. Errors and Exceptions — Python 3.12.13 documentation

We will now look at the else and finally clauses in the Python tutorial.

First, read the paragraph explaining the else clause. Then, skip down to the section "Defining Clean-up Actions" and read about the finally clause.

Python Tutorial: Using Try/Except Blocks for Error Handling

Now let's see these concepts in action with Corey Schafer's video.

Watch the segment from this timestamp. It clearly demonstrates how else runs only on success, while finally runs regardless of the outcome.

You might be thinking that finally seems like a lot of work to ensure a file gets closed. You're right! This is exactly why the with statement you learned in the last lesson is so powerful. When you write with open(...) as f:, Python is essentially creating a try...finally block for you behind the scenes, guaranteeing that f.close() is always called. This makes your code cleaner and safer.


Putting It All Together: A Robust State-File Reader

Let's refactor the workflow_state.json script from our previous lesson to make it truly robust using these new skills.

import json
from pathlib import Path
from datetime import datetime

STATE_FILE = Path("workflow_state.json")

def read_workflow_state(state_path: Path) -> datetime | None:
    """
    Safely reads the last run time from the state file.
    Returns the timestamp as a datetime object, or None if not found or invalid.
    """
    try:
        state_text = state_path.read_text(encoding="utf-8")
        state_data = json.loads(state_text)
        last_run_str = state_data["last_successful_run"]
        last_run_time = datetime.fromisoformat(last_run_str)
    
    except FileNotFoundError:
        # This is an expected condition on the first run.
        print("State file not found. Assuming this is the first run.")
        return None
        
    except (json.JSONDecodeError, KeyError) as e:
        # Catches two types of data errors: corrupted JSON or a missing key.
        print(f"Warning: Could not read state file due to a data error: {e}")
        print("Starting fresh.")
        return None
        
    except Exception as e:
        # A catch-all for any other unexpected problem.
        print(f"An unexpected error occurred while reading the state file: {e}")
        return None

    else:
        # This block runs only if the 'try' block was fully successful.
        print(f"Successfully loaded state. Last run was at: {last_run_time}")
        return last_run_time

# --- Main script logic ---
last_run = read_workflow_state(STATE_FILE)

print("\nPerforming main automation task...")
# (Imagine your script does some work here...)
print("Task completed successfully!")

# In a real script, we would now write the new state back to the file.

Try running this script. Then, create an empty workflow_state.json file and run it again. Then, put invalid JSON like {"key": "value} in it and run it again. See how the script handles each situation gracefully instead of crashing.


Conclusion

You've now added a critical layer of resilience to your automation skills. By anticipating and handling errors, you ensure your scripts can run reliably without constant supervision.

Key takeaways from this lesson:

  • Unhandled exceptions crash your program. Use try...except blocks to catch them and handle them gracefully.
  • Always be specific about the exceptions you catch (e.g., FileNotFoundError, KeyError). Avoid generic except Exception: which can hide bugs.
  • You can handle multiple error types with a tuple, like except (KeyError, ValueError):.
  • Use the else clause for code that should only run on success, and finally for cleanup code that must always run.
  • The with statement is a convenient, built-in form of try...finally for resource management.

Now that you can catch and handle errors, the next logical step is to record them. In our next lesson, we will dive into configuring Python's logging module to record timestamps, severity levels, and diagnostic context, turning silent failures into auditable events.