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:
- Python tries to execute the code inside the
tryblock. - If no exception occurs, the
exceptblock is skipped entirely. - If an exception occurs anywhere inside the
tryblock, Python immediately stops executing the rest of thetryblock and looks for a matchingexceptblock. - If a matching
exceptblock is found, its code is executed. - Execution then continues after the
try...exceptstructure.
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...exceptblocks 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 eto get details about the caught exception. - Use
elsefor code that should run only if thetryblock succeeds. - Use
finallyfor cleanup code that must always run. - Use
raiseto 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.