10xdev Book: Programming from Zero to Pro with Python

Chapter 9: Error and Exception Handling in Python 3.14

Chapter Introduction

Okay, so you’re writing code, things are going well, and then… CRASH. 💥 Maybe you tried to open a file that wasn’t there, divide by zero, or access a dictionary key that didn’t exist. These runtime errors, called exceptions, can stop your program dead in its tracks.

Just letting your program crash isn’t very professional or user-friendly. Exception handling is the technique programmers use to anticipate potential errors and deal with them gracefully. It’s like putting safety nets in place so that when something unexpected happens, your program can recover or at least shut down smoothly instead of just falling apart.

In this chapter, we’ll learn how to use Python 3.14’s try, except, else, and finally blocks to build more robust and resilient applications.


1. What’s an Exception?

An exception is an error that occurs while your program is running. Python signals that something went wrong by “raising” an exception object. If this exception isn’t “caught,” the program terminates and prints an error message (a traceback).

Common Exception Types You’ll Encounter:

  • FileNotFoundError: Tried to open a non-existent file in read mode.
  • ZeroDivisionError: Divided (or modulo) by zero.
  • ValueError: An operation received an argument with the right type but an inappropriate value (e.g., int("abc")).
  • TypeError: An operation was performed on an object of an inappropriate type (e.g., "hello" + 5).
  • IndexError: Tried to access a list/tuple index that’s out of bounds.
  • KeyError: Tried to access a dictionary key that doesn’t exist.
  • AttributeError: Tried to access an attribute or method that doesn’t exist on an object.

Python 3.14+ continues to improve error messages, often giving you very specific hints about what might have gone wrong, like suggesting correct attribute names if you made a typo.


2. Catching Exceptions: try and except

The core mechanism for handling exceptions is the try...except block.

How it Works:

  1. Python tries to execute the code inside the try block.
  2. If no exception occurs, the except block is skipped entirely.
  3. If an exception occurs anywhere inside the try block, Python immediately stops executing the rest of the try block and looks for a matching except block.
  4. If a matching except block is found, its code is executed.
  5. Execution then continues after the try...except structure.

Basic Example:

try:
    age_str: str = input("Enter your age: ")
    age: int = int(age_str) # This line might raise a ValueError
    print(f"Next year, you will be {age + 1}.")
except ValueError:
    # This block runs ONLY if int() fails
    print("Oops! That wasn't a valid whole number. Please enter digits only.")

print("Program continues...") # This line runs regardless of the exception

3. Handling Specific Exceptions

Catching all possible exceptions with a bare except: is generally a bad idea. It can hide bugs by catching errors you weren’t expecting. It’s much better to catch only the specific exceptions you anticipate.

You can have multiple except blocks to handle different error types:

num_str: str = input("Enter a number to divide 10 by: ")

try:
    num: float = float(num_str) # Potential ValueError
    result: float = 10 / num   # Potential ZeroDivisionError
    print(f"10 divided by {num} is {result}")

except ValueError:
    print("Invalid input. Please enter a number.")
except ZeroDivisionError:
    print("You can't divide by zero!")

Python 3.14 Syntax Simplification: In Python 3.14 and later, if you want to catch multiple specific exceptions with the same handling code, you no longer need to enclose them in parentheses.

# Older Python (and still valid in 3.14+)
# except (ValueError, TypeError) as e:
#    print(f"Data error: {e}")

# Python 3.14+ alternative syntax
# except ValueError, TypeError as e:
#    print(f"Data error: {e}")

While the new syntax is available, sticking with the parentheses except (ValueError, TypeError) is often considered slightly clearer and is compatible with older versions.


4. Getting Error Details (as e)

You can capture the actual exception object (which contains details about the error) using as variable_name:

try:
    result = 10 / 0
except ZeroDivisionError as e:
    print("An error occurred!")
    print(f"Error type: {type(e)}")
    print(f"Error details: {e}") # The object itself often converts to a useful string

5. The else Block: Code for Success

Sometimes, you have code that should only run if the try block completed without raising any exceptions. That’s what the optional else block is for.

file_path = "maybe_exists.txt"
try:
    print(f"Attempting to open {file_path}...")
    with open(file_path, 'r', encoding='utf-8') as f:
        content = f.read()
except FileNotFoundError:
    print("File could not be found.")
except IOError as e:
    print(f"Error reading file: {e}")
else:
    # This runs ONLY if the 'try' was successful
    print("File read successfully!")
    print(f"Content length: {len(content)}")

This is often cleaner than putting the success-dependent code inside the try block itself.


6. The finally Block: Always Runs

The optional finally block contains code that always executes, regardless of whether an exception occurred in the try block or if it was caught by an except block. It even runs if you use return, break, or continue inside the try or except blocks.

It’s typically used for cleanup actions that must happen, like closing network connections or releasing resources (though with open() already handles file closing automatically!).

connection = None # Placeholder for a resource
try:
    print("Attempting to use resource...")
    # connection = open_resource() # Hypothetical function
    # result = 10 / int(input("Enter divisor: ")) # Potential errors
    print("Resource used successfully.") # May not run if error occurs
except (ValueError, ZeroDivisionError) as e:
    print(f"An error occurred: {e}")
finally:
    # This ALWAYS runs
    print("Executing cleanup actions...")
    # if connection:
    #    connection.close()
    print("Cleanup finished.")

7. Raising Exceptions (raise)

You don’t just have to catch exceptions; you can (and should) raise them yourself when something invalid occurs according to your program’s logic. This signals an error condition clearly.

def calculate_discount(price: float, percentage: float) -> float:
    """Applies discount, raising ValueError for invalid percentages."""
    if not 0 <= percentage <= 1:
        # Raise an exception if the input is invalid
        raise ValueError("Discount percentage must be between 0 (0%) and 1 (100%).")
    return price * (1 - percentage)

# Now, code calling this function needs to handle the potential error
try:
    final_price = calculate_discount(100.0, 1.2) # Invalid percentage (120%)
    print(f"Final price: {final_price}")
except ValueError as e:
    print(f"Error applying discount: {e}")

Raising specific exceptions makes your functions more robust and communicates errors clearly to the code that uses them.


8. Chapter Summary

  • Exceptions are runtime errors. Unhandled exceptions crash your program.
  • Use try...except blocks to catch and handle specific exceptions gracefully.
  • Catch specific exception types rather than using a bare except:. Python 3.14 offers slightly cleaner syntax for multiple exceptions, but parentheses (Error1, Error2) remain common.
  • Use as e to get details about the caught exception.
  • Use else for code that should run only if the try block succeeds.
  • Use finally for cleanup code that must always run.
  • Use raise to signal errors within your own code based on invalid conditions or inputs.

You now know how to make your Python 3.14 programs resilient to errors. But how do professional developers structure larger applications? The next step is to explore Object-Oriented Programming (OOP), a powerful paradigm for modeling real-world entities and their interactions in your code.