Skip to content

Python 3 - Exceptions

Errors are inevitable in programming. Python provides robust mechanisms for dealing with errors that occur during program execution. These unexpected events are called exceptions.

Two key features for managing errors and debugging are:

  • Exception Handling: Using try, except, else, and finally blocks to gracefully manage runtime errors.
  • Assertions: Using the assert statement for internal self-checks during development and testing.

An exception is an event that occurs during the execution of a program that disrupts the normal flow of instructions. When Python encounters a situation it cannot cope with (e.g., dividing by zero, accessing a non-existent file, using an incorrect index), it raises an exception.

An exception is represented by a Python object, typically an instance of a class derived from the base Exception class. If an exception is raised and not handled by the program, the program terminates and prints a traceback, which shows the sequence of calls that led to the error.

Python has many built-in exceptions for common errors. Here are some frequently encountered ones:

Exception NameDescription
ExceptionThe base class for most user-defined and built-in exceptions (excluding system-exiting ones).
AttributeErrorRaised when an attribute reference or assignment fails (e.g., obj.nonexistent_method()).
EOFErrorRaised when the input() function hits an end-of-file condition without reading any data.
ImportErrorRaised when an import statement fails to find the module or a name within the module.
IndexErrorRaised when a sequence subscript (index) is out of range (e.g., my_list[10] for a list with 5 elements).
KeyErrorRaised when a dictionary key is not found (e.g., my_dict['nonexistent_key']).
KeyboardInterruptRaised when the user hits the interrupt key (usually Ctrl+C).
NameErrorRaised when a local or global name (variable, function) is not found.
OSErrorBase class for OS-related errors (e.g., file not found, permission denied). Subclasses like FileNotFoundError, PermissionError provide more specifics.
SyntaxErrorRaised by the parser when a syntax error is encountered.
IndentationErrorSubclass of SyntaxError. Raised when indentation is incorrect.
TypeErrorRaised when an operation or function is applied to an object of an inappropriate type (e.g., 'hello' + 5).
ValueErrorRaised when an operation or function receives an argument of the correct type but an inappropriate value (e.g., int('abc')).
ZeroDivisionErrorRaised when the second argument of a division or modulo operation is zero.

To handle exceptions gracefully, you place code that might raise an exception inside a try block. Following the try block, you add one or more except blocks to specify how to handle particular types of exceptions.

try:
# Code that might raise an exception
# ... suspicious operations here ...
pass
except SpecificExceptionType1:
# Code to run if SpecificExceptionType1 occurs
pass
except (SpecificExceptionType2, SpecificExceptionType3):
# Code to run if either Type2 or Type3 occurs
pass
except AnotherExceptionType as e:
# Code to run if AnotherExceptionType occurs
# 'e' holds the exception object (contains details)
print(f"An error occurred: {e}")
except Exception as e:
# Catch any other exception derived from Exception (use cautiously)
print(f"An unexpected error occurred: {e}")

Key points:

  • Python tries to execute the code in the try block.
  • If an exception occurs, Python looks for a matching except block.
  • The first matching except block is executed.
  • If no matching except block is found, the exception propagates up the call stack (potentially terminating the program).
  • Using a bare except: (without specifying an exception type) is generally discouraged as it catches all exceptions, including system ones like SystemExit or KeyboardInterrupt, making it hard to debug or stop the program.
  • Catching specific exceptions is preferred.

Example: Handling Division by Zero and Invalid Input

Section titled “Example: Handling Division by Zero and Invalid Input”
#!/usr/bin/env python3
try:
numerator_str = input("Enter the numerator: ")
denominator_str = input("Enter the denominator: ")
numerator = int(numerator_str)
denominator = int(denominator_str)
result = numerator / denominator
print(f"The result is: {result}")
except ValueError:
print("Error: Please enter valid integers for numerator and denominator.")
except ZeroDivisionError:
print("Error: Cannot divide by zero.")
except Exception as e:
print(f"An unexpected error occurred: {e}")
print("Execution continues after the try-except block.")

Try running this code with different inputs (e.g., ‘10’ and ‘2’, ‘10’ and ‘0’, ‘10’ and ‘abc’) to see how the different except blocks handle the errors.

You can optionally include an else block after the except block(s). The code in the else block executes only if the try block completes without raising any exceptions.

This is useful for code that should run only when the try block succeeds, separating it from the potentially error-prone code.

try:
# Risky operations
risky_operation()
except SomeError:
print("Handling SomeError...")
else:
# This runs ONLY if no exceptions occurred in the try block
print("Operation successful, proceeding...")
perform_next_step()
#!/usr/bin/env python3
filename = "my_data.txt"
try:
# Using 'with' ensures the file is closed even if errors occur
with open(filename, 'r') as f:
content = f.read()
except FileNotFoundError:
print(f"Error: File '{filename}' not found.")
except OSError as e:
print(f"Error reading file '{filename}': {e}")
else:
# Process content only if file was opened and read successfully
print(f"File '{filename}' read successfully.")
# print(f"Content: {content[:50]}...") # Process the content

An optional finally block can be added. The code within the finally block is always executed, regardless of whether an exception occurred in the try block or if it was handled by an except block. It even runs if the try or except block uses return, break, or continue.

The finally clause is typically used for cleanup actions that must happen under all circumstances, such as closing files or network connections, releasing locks, etc.

try:
# Operations, potentially including resource allocation
f = open("important_file.txt", "w")
f.write("Some data")
# Maybe an error occurs here: result = 10 / 0
except ZeroDivisionError:
print("Caught division by zero.")
finally:
# This cleanup code ALWAYS runs
print("Ensuring file is closed.")
if 'f' in locals() and not f.closed:
f.close()

Note: While finally is essential for cleanup, modern Python often uses context managers (the with statement) for resources like files, which automatically handle cleanup, making explicit finally blocks less common for this specific purpose. The example above demonstrates the concept; using with open(...) is generally preferred for file I/O.

A try statement can have except clauses and a finally clause. The order is try -> except(s) -> else -> finally.

You can deliberately raise an exception using the raise statement. This is useful when you detect an error condition in your own code or want to re-raise an exception after catching it.

# Raise a specific exception type with a message
raise ValueError("Invalid input value provided")
# Raise an instance of an exception class
error_obj = TypeError("Incompatible types")
raise error_obj
# Re-raise the last exception that was active (often used in except blocks)
try:
do_something()
except Exception as e:
print("Logging the error...")
# Perform some logging or partial handling
raise # Re-raise the original exception 'e'
#!/usr/bin/env python3
def calculate_area(width, height):
if not isinstance(width, (int, float)) or not isinstance(height, (int, float)):
raise TypeError("Width and height must be numeric.")
if width <= 0 or height <= 0:
raise ValueError("Width and height must be positive.")
return width * height
try:
area = calculate_area(10, 5)
print(f"Area: {area}")
area = calculate_area(10, -5)
# area = calculate_area(10, "abc") # Uncomment to test TypeError
print(f"Area: {area}")
except (ValueError, TypeError) as e:
print(f"Error calculating area: {e}")

Output:

Area: 50
Error calculating area: Width and height must be positive.

You can create your own custom exception types by defining new classes that inherit from Exception (or a more specific built-in exception class). This allows you to create more meaningful error types specific to your application or library.

#!/usr/bin/env python3
# Define a custom exception class
class InsufficientFundsError(Exception):
"""Custom exception for when an account has insufficient funds."""
def __init__(self, balance, amount_needed):
self.balance = balance
self.amount_needed = amount_needed
message = f"Attempted to withdraw {amount_needed}, but only {balance} available."
super().__init__(message) # Call the base Exception constructor
# Example usage
account_balance = 100
withdrawal_amount = 150
try:
if withdrawal_amount > account_balance:
raise InsufficientFundsError(account_balance, withdrawal_amount)
else:
account_balance -= withdrawal_amount
print(f"Withdrawal successful. New balance: {account_balance}")
except InsufficientFundsError as e:
print(f"Transaction Failed: {e}")
# You can access custom attributes if needed:
# print(f"Current balance: {e.balance}, Amount needed: {e.amount_needed}")

Output:

Transaction Failed: Attempted to withdraw 150, but only 100 available.

Assertions (assert) are primarily used during development and testing as internal sanity checks. They verify conditions that should always be true if the program is behaving correctly.

Syntax: assert condition, [optional_message]

  • If the condition evaluates to True, the assertion does nothing.
  • If the condition evaluates to False, Python raises an AssertionError with the optional message.
  • Assertions can be globally disabled (for performance in production) by running Python with the -O (optimize) flag. Therefore, they should not be used for input validation or error handling that must always occur.
#!/usr/bin/env python3
def calculate_discount(price, discount_percentage):
assert 0 <= discount_percentage <= 100, "Discount percentage must be between 0 and 100."
discount_amount = price * (discount_percentage / 100)
return price - discount_amount
# Valid call
final_price = calculate_discount(50, 10)
print(f"Final price (10% off): {final_price}")
# Invalid call - will raise AssertionError if assertions are enabled
try:
final_price = calculate_discount(50, 110)
print(f"Final price (110% off): {final_price}")
except AssertionError as e:
print(f"Assertion Failed: {e}")

Output (when assertions are enabled):

Final price (10% off): 45.0
Assertion Failed: Discount percentage must be between 0 and 100.