Skip to content

Python Exceptions

Errors are inevitable in programming. Python provides robust mechanisms to handle runtime errors gracefully, preventing your program from crashing unexpectedly.

  • Exception Handling: Using try, except, else, and finally blocks to manage errors as they occur.
  • Assertions: Using the assert statement for internal self-checks during development and debugging.

An exception is an event that occurs during the execution of a program that disrupts the normal flow of instructions. When Python encounters an error condition (like dividing by zero or trying to access a non-existent file), it creates an exception object.

If this exception object is not ‘caught’ and handled by your code, the program terminates and prints a traceback (an error message showing where the error occurred).

Python has many built-in exceptions for common errors. Understanding these helps in writing specific except blocks. Here are a few examples:

Exception NameDescription
ExceptionThe base class for most built-in, non-system-exiting exceptions.
AttributeErrorRaised when an attribute reference or assignment fails (e.g., mylist.non_existent_method()).
ImportErrorRaised when an import statement fails to find the module or a name within the module.
ModuleNotFoundErrorSubclass of ImportError, raised when a module cannot be located.
IndexErrorRaised when a sequence subscript (index) is out of range (e.g., mylist[10] when list has only 3 items).
KeyErrorRaised when a dictionary key is not found (e.g., mydict['non_existent_key']).
NameErrorRaised when a local or global name (variable, function) is not found.
TypeErrorRaised when an operation or function is applied to an object of inappropriate type (e.g., '2' + 2).
ValueErrorRaised when an operation or function receives an argument of the right type but inappropriate value (e.g., int('abc')).
ZeroDivisionErrorRaised when the second argument of a division or modulo operation is zero.
FileNotFoundErrorSubclass of OSError, raised when trying to open a file that doesn’t exist.
OSErrorBase class for OS-related errors (like file not found, permission denied).
KeyboardInterruptRaised when the user hits the interrupt key (usually Ctrl+C).

To handle potential errors, you place the code that might raise an exception inside a try block. You then follow it with one or more except blocks to specify how to handle different types of exceptions.

try:
# Code that might raise an exception
# ... operations ...
result = 10 / risky_variable
file = open("maybe_missing.txt", "r")
# ... more operations ...
except ZeroDivisionError:
# Handle division by zero specifically
print("Error: Cannot divide by zero!")
result = 0
except FileNotFoundError:
# Handle missing file specifically
print("Error: File not found!")
# Maybe provide a default or exit
except Exception as e:
# Catch any other exceptions that inherit from Exception
print(f"An unexpected error occurred: {e}")
# Log the error, provide a generic message, etc.
# Code here continues execution if an exception was caught

Key Points:

  • Python tries to execute the code in the try block.
  • If an exception occurs, Python stops executing the try block and looks for a matching except block.
  • It checks the except blocks in order. The first one whose type matches the raised exception (or is a base class of it) is executed.
  • If a matching except block is found and executed, the program continues after the try...except structure (unless the except block itself raises an exception or exits).
  • If no matching except block is found, the exception propagates up the call stack. If uncaught, the program terminates.
  • Using a specific exception type (ZeroDivisionError) is generally better than a broad one (Exception) because it allows tailored error handling and avoids accidentally catching unrelated errors.
  • The as e clause captures the exception object into the variable e, allowing access to details about the error.

You can add an optional else block after all except blocks. The code in the else block executes only if the try block completes without raising any exceptions.

try:
# Operations that might fail
value = int(input("Enter a number: "))
result = 100 / value
except ValueError:
print("Invalid input. Please enter a number.")
except ZeroDivisionError:
print("Cannot divide by zero.")
else:
# Executes only if try block succeeds
print(f"Result is: {result}")
print("Operation successful.")
print("Program continues...")

The else block is useful for code that should run only when the try block was successful, separating it from the potentially risky code.

An optional finally block can be added. The code in the finally block always executes, 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.

file = None # Initialize file variable outside try
try:
print("Attempting to open file...")
file = open("my_data.txt", "w")
file.write("Writing important data!")
# Simulate an error potentially happening after opening
# result = 10 / 0
except FileNotFoundError:
print("Error: Could not find the file path to write.")
except Exception as e:
print(f"An error occurred during writing: {e}")
finally:
# This block always executes for cleanup
print("Entering finally block...")
if file and not file.closed:
print("Closing the file.")
file.close()
else:
print("File was not opened or already closed.")
print("Program finished.")

finally is typically used for cleanup actions, such as closing files or network connections, ensuring resources are released properly.

Note: A try block must have at least one except or finally clause. It can have except clauses, a finally clause, or both. If both are present, the finally clause executes last.

As seen in the examples with except Exception as e:, you can capture the exception object itself. This object often contains useful information about the error.

try:
x = int('not_a_number')
except ValueError as ve:
print(f"Caught a ValueError!")
print(f"Error details: {ve}") # Prints the standard error message associated with the exception
print(f"Arguments: {ve.args}") # Exceptions store arguments, often including the message

This produces output like:

Caught a ValueError!
Error details: invalid literal for int() with base 10: 'not_a_number'
Arguments: ("invalid literal for int() with base 10: 'not_a_number'",)

You can also deliberately raise exceptions in your own code using the raise statement. This is useful when you detect an error condition that prevents your function or method from fulfilling its contract.

raise ExceptionType("Optional descriptive message about the error")
def check_positive(number):
if number <= 0:
# Raise a specific, appropriate exception
raise ValueError("Input must be a positive number.")
print(f"{number} is positive.")
try:
check_positive(10)
check_positive(-5)
except ValueError as e:
print(f"Error caught: {e}")

Best practice is to raise specific, built-in exceptions (like ValueError, TypeError) when appropriate, or create custom exceptions for application-specific errors.

Note: Raising exceptions using strings (raise "Invalid level!") is deprecated and should not be used in modern Python.

For errors specific to your application or library, you can create custom exception classes by inheriting from Exception or a more specific built-in exception class.

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

Custom exceptions make your error handling more specific and informative.

An assertion is a debugging aid that tests a condition. If the condition is True, nothing happens. If the condition is False, the assert statement raises an AssertionError exception with an optional message.

Assertions are primarily used to check for internal errors or violations of assumptions (programmer errors), not for expected error conditions (like invalid user input or missing files, which should be handled by try...except).

assert condition, "Optional message if condition is False"
def calculate_discount(price, discount_percentage):
# Pre-conditions check: ensure inputs are valid for the logic
assert price >= 0, "Price cannot be negative"
assert 0 <= discount_percentage <= 100, "Discount must be between 0 and 100"
discount_amount = price * (discount_percentage / 100)
final_price = price - discount_amount
# Post-condition check: ensure result makes sense
assert 0 <= final_price <= price, "Final price calculation seems wrong"
return final_price
# Valid call
print(f"Discounted price: {calculate_discount(100, 10)}")
# Invalid call - will raise AssertionError
try:
print(calculate_discount(100, 110))
except AssertionError as e:
print(f"Assertion Failed: {e}")

Important Note: Assertions can be globally disabled in Python by running the interpreter with the -O (optimize) flag (python -O your_script.py). Therefore, they should not be used for error handling logic that must always be present.