Skip to main content

Welcome to a Grasp lesson

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

Lesson 7

Handling Runtime Errors with Try-Except

Recommended resources

Welcome back! In our last lesson, we focused on functions, the fundamental building blocks for creating reusable and organized code. We saw how they can take inputs, perform tasks, and return results. You also saw a brief example where a function could fail by raising an error if given incorrect input.

This lesson directly addresses that scenario. As a Quality Assurance professional, you know that a critical aspect of robust software is its ability to handle unexpected situations gracefully. A test script that crashes halfway through because an element wasn't found or an API returned an error is unreliable. This lesson introduces the mechanism Python provides to build that resilience into your code: exception handling. You will learn how to anticipate and manage runtime errors using try and except blocks, ensuring your automation scripts can handle failures intelligently instead of just stopping.

When Code Goes Wrong: Syntax Errors vs. Exceptions

In Python, not all errors are the same. It's crucial to distinguish between two main types:

  1. Syntax Errors: These are grammatical mistakes in your code. You might forget a colon, misspell a keyword, or have mismatched parentheses. The Python interpreter detects these before your program starts running and will refuse to execute it. Your IDE, like PyCharm, will usually highlight these for you as you type.

  2. Exceptions: These are errors that occur during the execution of a syntactically correct program. The code is valid Python, but a situation arises that the program cannot handle. A classic example is trying to divide a number by zero. The syntax is correct, but the operation is mathematically impossible.

The video "Exception Handling in Python" from Software Testing Mentor provides a clear, practical distinction between these two.

Python for Testers #39 - Exception Handling in Python

This video starts by explaining the difference between syntax errors and runtime exceptions using a simple division example, which is a perfect introduction to our topic.

Watch the first segment, from the beginning to this point, to see a live demonstration of a ZeroDivisionError and understand why it's considered an exception, not a syntax error.

When an exception occurs and is not handled, your program "crashes" and prints a traceback message. While this traceback is useful for debugging, it's not what you want to happen in a running automation suite. We need a way to catch these exceptions and decide what to do next.


The try and except Blocks: Catching Errors

The primary tool for handling exceptions in Python is the try...except block. The logic is straightforward:

  • The try block contains the code that you suspect might raise an exception.
  • The except block contains the code that runs if and only if an exception occurs in the try block.

This allows you to "catch" the error and prevent it from crashing your program. You can then log the error, attempt a recovery action, or simply mark a test as failed and move on.

The article "Python Exceptions: An Introduction" from Real Python offers a thorough explanation of this core concept.

Python Exceptions: An Introduction

This article section explains the fundamental try and except structure and, most importantly, emphasizes the best practice of catching specific exceptions.

Please read the section titled Handling Exceptions With the try and except Block. Pay close attention to the argument against using a bare except: clause. Understanding why it's better to catch specific errors like FileNotFoundError or RuntimeError is crucial for writing maintainable and debuggable test code.

As you've just read, being specific about the exceptions you catch is vital. A broad except: can hide bugs you weren't expecting. For example, in a test, you might expect an element to be "not found," but a generic except block could also hide a completely different error, like a network failure, making the root cause of the test failure difficult to diagnose.

Let's watch a clear demonstration from Corey Schafer on how to handle specific exceptions and even catch multiple different types of exceptions in a structured way.

Python Tutorial: Using Try/Except Blocks for Error Handling

This video provides excellent examples of handling FileNotFoundError and shows the syntax for layering multiple except blocks to handle different potential errors.

First, watch how a try...except block is used to handle a potential error gracefully instead of crashing. Then, watch the next segment on catching specific exceptions. This part clearly demonstrates why catching a general Exception can be misleading and how to stack except blocks for different error types, like FileNotFoundError and NameError.


The Full Structure: else and finally

The try...except block can be extended with two more optional clauses: else and finally. These give you finer control over your code's execution flow.

This diagram shows the complete control flow for exception handling in Python. The `try` block is for the main code, `except` handles errors, `else` runs only on success, and `finally` runs regardless of the outcome.
  • The else Block: This code runs only if the try block completes successfully, meaning no exceptions were raised. This is useful for separating your "happy path" logic from your error-handling logic. For a test, you might put the successful validation assertions in the else block.

  • The finally Block: This code runs no matter what. It executes whether an exception occurred or not. This is extremely important for cleanup actions. In test automation, you would use finally to ensure that resources are released, such as closing a browser window, shutting down a database connection, or writing a report file, even if the test itself failed.

This pattern is demonstrated perfectly in the following code image.

This code defines a function `attempt_division`. When called with `(5, 1)`, no error occurs: the `try` block succeeds, the `else` block prints the result, and `finally` runs. When called with `(5, 0)`, a `ZeroDivisionError` occurs: the `except` block handles it, and `finally` still runs.

The video from Software Testing Mentor explains the roles of else and finally with examples directly relevant to your work.

Python for Testers #39 - Exception Handling in Python

This video continues from where we left off, explaining the full try...except...else...finally structure.

First, watch the quick intro to the basic try-except syntax. Next, see the explanation of the else block, which executes only when there is no exception. Finally, watch the segment on the finally block. The instructor gives excellent examples of its use for cleanup, such as closing files or database connections, which is a key practice in robust automation.


Proactively raise-ing Exceptions

Sometimes, you may want to trigger an exception yourself based on a specific condition in your code. For instance, if a function receives invalid input, you can raise an exception to signal that something is wrong. This is clearer than returning an error code and forces the calling code to handle the problem.

Python for Testers #39 - Exception Handling in Python

This short segment shows how to use the raise keyword to create your own exceptions based on logical conditions.

Watch the section from this timestamp to see how you can check a condition (like y == 0) and manually raise an exception with a custom message.


Let's Practice

Imagine you are writing a helper function for a test suite. This function validates results from a dictionary that stores test outcomes. We need to make it robust against missing data or incorrect data types.

Your Task:
Complete the function get_final_score below. It takes a dictionary of scores and a test ID. It should:

  1. Try to retrieve the score for the given test_id.
  2. Handle a KeyError if the test_id doesn't exist in the dictionary, printing an informative message.
  3. Handle a TypeError if the score is not a number (e.g., None or a string), as we can't perform calculations on it. Print a different message for this case.
  4. If the score is successfully retrieved and is a number, use the else block to print a "Validation successful" message along with the score.
  5. Use the finally block to always print "Finished score validation attempt."

Here is the code to get you started:

def get_final_score(scores_data, test_id):
    """
    Retrieves and validates a test score from a dictionary.
    Handles potential KeyError and TypeError.
    """
    print(f"\n--- Checking test ID: {test_id} ---")
    try:
        score = scores_data[test_id]



        # This line will raise a TypeError if score is not a number
        calculated_score = score * 1.0 
    except KeyError:
        print(f"Error: Test ID '{test_id}' not found.")
    except TypeError:
        print(f"Error: Score for '{test_id}' is not a valid number. Found: {score}")
    else:
        print(f"Validation successful. Score: {calculated_score}")
    finally:
        print("Finished score validation attempt.")




# --- Data for testing ---
test_scores = {
    "login_test_01": 98,
    "payment_test_05": 76,
    "search_test_02": "incomplete", # Invalid data type
    "dashboard_test_03": None      # Invalid data type
}




# --- Let's run the function with different inputs ---
get_final_score(test_scores, "login_test_01")      # Success case
get_final_score(test_scores, "checkout_test_99")  # Key error case
get_final_score(test_scores, "search_test_02")    # Type error case
get_final_score(test_scores, "dashboard_test_03") # Type error case

Expected Output:

--- Checking test ID: login_test_01 ---
Validation successful. Score: 98.0
Finished score validation attempt.

--- Checking test ID: checkout_test_99 ---
Error: Test ID 'checkout_test_99' not found.
Finished score validation attempt.

--- Checking test ID: search_test_02 ---
Error: Score for 'search_test_02' is not a valid number. Found: incomplete
Finished score validation attempt.

--- Checking test ID: dashboard_test_03 ---
Error: Score for 'dashboard_test_03' is not a valid number. Found: None
Finished score validation attempt.

Conclusion

You have now learned how to write more resilient and robust Python code. By anticipating and handling runtime errors, you can prevent your automation scripts from crashing unexpectedly, which is a cornerstone of a reliable and professional testing framework.

Here are the key takeaways:

  • Exceptions are errors that occur during program execution.
  • Use a try block to wrap code that might fail.
  • Use an except SpecificError block to catch and handle a particular type of error. Avoid generic except blocks.
  • Use an else block for code that should only run if the try block was successful.
  • Use a finally block for cleanup code that must run regardless of whether an error occurred.

In our next lesson, we will put this knowledge to immediate use as we learn about reading from and writing to text files. File operations are a prime example of where exception handling is not just useful, but essential. What if the file you want to read doesn't exist (FileNotFoundError)? What if you don't have permission to write to a location (PermissionError)? You are now equipped to handle these scenarios.