Contents

Python Exception Handling

In Python programming, errors can be broadly classified into two types: Syntax Errors and Exceptions. Errors halt the program execution, while exceptions arise from unexpected events that disrupt the usual program flow but may still be handled to allow the program to continue.

Common Types of Python Exceptions

Python has several built-in exceptions for handling errors that may occur during code execution. Here are some frequent ones:

  • SyntaxError: Raised when there is a syntax issue, like a missing colon or an unmatched parenthesis.
  • TypeError: Occurs when an operation is applied to an inappropriate data type, such as adding an integer to a string.
  • NameError: Triggered when a variable or function name is not found.
    IndexError: Occurs when an index is out of range in a list or other sequence.
  • KeyError: Raised when a key is not found in a dictionary.
  • ValueError: Occurs when a function receives an argument with the right type but an inappropriate value.
  • AttributeError: Triggered when an attribute or method is not found on an object.
  • IOError: Raised for input/output errors, such as issues reading or writing a file.
  • ZeroDivisionError: Occurs when dividing a number by zero.
  • ImportError: Triggered when a module fails to import.
Difference Between Syntax Errors and Exceptions

Syntax Errors: Caused by improper syntax and will terminate the program.

				
					amount = 5000
if amount > 1000
    print("Eligible to make a purchase")

				
			

Output:

				
					SyntaxError: expected ':'

				
			

Exceptions: These arise from runtime errors in syntactically correct code, which can change the program’s normal flow.

				
					value = 100
result = value / 0
print(result)

				
			

Output:

				
					ZeroDivisionError: division by zero

				
			

Handling Exceptions with try and except

In Python, you use try and except blocks to catch and handle exceptions.

Example:

				
					data = [1, 2, 3]
try:
    print("Second element:", data[1])
    print("Fourth element:", data[3])
except IndexError:
    print("An error occurred: Index out of range")

				
			

Output:

				
					Second element: 2
An error occurred: Index out of range

				
			

Catching Specific Exceptions

You can use multiple except clauses to handle different types of exceptions.

				
					def calculate(a):
    if a < 5:
        result = a / (a - 4)
    print("Result:", result)

try:
    calculate(3)
    calculate(5)
except ZeroDivisionError:
    print("Division by zero error occurred and handled")
except NameError:
    print("NameError occurred and handled")

				
			

Output:

				
					Division by zero error occurred and handled

				
			
try with else Clause

The else clause runs only if the try block does not raise any exceptions.

				
					name = 'Sam'
age = 25
print(f"Hello, My name is {name} and I'm {age} years old.")

				
			

Output:

				
					-5.0
Division resulted in zero denominator

				
			

finally Clause in Python

The finally block is always executed after try and except blocks, regardless of whether an exception occurs.

				
					num = int(input("Enter a value: "))
add = num + 5
print("The sum is %d" % add)

				
			

Output:

				
					Cannot divide by zero
Execution complete

				
			

Raising Exceptions

The raise statement allows you to force a specific exception. You can specify an error message when raising an exception.

				
					try:
    raise NameError("This is a custom NameError")
except NameError:
    print("A NameError occurred")
    raise

				
			

Output:

				
					A NameError occurred
Traceback (most recent call last):
  ...
NameError: This is a custom NameError

				
			
Advantages of Exception Handling
  • Improves program reliability: Helps in avoiding program crashes from unexpected errors.
  • Simplifies error handling: Keeps error management separate from main logic, making the code more readable.
  • Produces cleaner code: Reduces complex conditional checks for error detection.
  • Eases debugging: Exception traceback indicates the error location, simplifying debugging.
Disadvantages of Exception Handling
  • Performance overhead: Handling exceptions can be slower than using conditional checks.
  • Increases code complexity: Multiple exceptions can add complexity, especially with varied handling methods.
  • Potential security risks: Unhandled exceptions might reveal sensitive information or create vulnerabilities, so careful handling is essential.

User-defined Exceptions in Python with Examples

Custom exceptions in Python with practical examples is as follows.

				
					class CustomError(Exception):
    pass

raise CustomError("Example of Custom Exceptions in Python")

				
			

Output:

				
					CustomError: Example of Custom Exceptions in Python

				
			

Python raises errors and exceptions when something goes wrong, potentially causing abrupt program termination. However, with try-except blocks, Python provides robust exception handling to prevent such interruptions. Common exceptions include IndexError, ImportError, IOError, ZeroDivisionError, TypeError, and FileNotFoundError.

Creating User-Defined Exceptions in Python

Custom exceptions should inherit from the Exception class, directly or indirectly. While it’s not required, custom exceptions often follow the naming convention of ending with “Error,” similar to Python’s standard exceptions.

Example:

				
					# A Python program to create a user-defined exception
# MyError class inherits from Exception

class MyError(Exception):
    # Constructor to initialize the error value
    def __init__(self, value):
        self.value = value

    # __str__ method to return the error message
    def __str__(self):
        return repr(self.value)

try:
    raise MyError(3 * 2)

# Handling the exception
except MyError as error:
    print('A new exception occurred:', error.value)

				
			

Output:

				
					Customizing Exception Classes
For more details about Exception class, try running:

python
Copy code

				
			
Customizing Exception Classes

For more details about Exception class, try running:

				
					help(Exception)

				
			

Example 1: User-Defined Exception with Multiple Inheritance

Below is an example with a base class Error inherited by user-defined classes to handle specific custom exceptions.

				
					# Defining a base class for exceptions
class Error(Exception):
    """Base class for other exceptions"""
    pass

class ZeroDivisionErrorCustom(Error):
    """Raised when the input value is zero"""
    pass

try:
    num = int(input("Enter a number: "))
    if num == 0:
        raise ZeroDivisionErrorCustom
except ZeroDivisionErrorCustom:
    print("Input value is zero, please try again!")

				
			

Output:

				
					Enter a number: 0
Input value is zero, please try again!

				
			

Example 2: Deriving Errors from Superclass Exception

When a module requires specific exception handling, a base class for those exceptions can be defined. Additional subclasses allow for distinct error conditions.

				
					# Define a base exception class
class Error(Exception):
    pass

class TransitionError(Error):
    # Raised when an invalid state transition occurs
    def __init__(self, prev, next, msg):
        self.prev = prev
        self.next = next
        self.msg = msg

try:
    raise TransitionError(2, 6, "Transition Not Allowed")

except TransitionError as error:
    print('Exception occurred:', error.msg)

				
			

Output:

				
					Exception occurred: Transition Not Allowed

				
			
Using Standard Exceptions as a Base Class

The following example demonstrates using RuntimeError as a base class for custom exceptions, like NetworkError.

				
					# NetworkError is derived from RuntimeError
class NetworkError(RuntimeError):
    def __init__(self, message):
        self.message = message

try:
    raise NetworkError("Network connection lost")

except NetworkError as e:
    print(e.message)

				
			

Output:

				
					Network connection lost

				
			
Advantages of Exception Handling
  • Reliability: Manages unexpected errors, ensuring program stability.
  • Simplified Error Management: Separates error-handling code from main logic.
  • Cleaner Code: Avoids nested conditionals by handling errors systematically.
  • Debugging Ease: Tracebacks help in pinpointing error locations quickly.
Disadvantages of Exception Handling
  • Performance Overhead: Handling exceptions is slightly slower than conditional checks.
  • Complexity: Can increase code complexity, especially with multiple exception types.
  • Security Risks: Improperly managed exceptions may expose sensitive information, so they must be handled with caution.

Built-in Exceptions in Python

In Python, all instances must be derived from the BaseException class, and no two unrelated exception classes are considered equivalent, even if they share the same name. The Python interpreter or built-in functions can raise these exceptions.

You can view built-in exceptions, functions, and attributes using the locals() function:

				
					>>> locals()['__builtins__']

				
			
Base Classes

The following exceptions serve primarily as base classes for other exceptions:

1. BaseException: This is the base class for all built-in exceptions. It is not intended for direct inheritance by user-defined classes, which should inherit from Exception. This class uses str() to create a string representation of the exception based on its arguments. An empty string is returned if there are no arguments.

Attributes:

  • args: A tuple of arguments passed to the exception constructor.
  • with_traceback(tb): Sets tb as the new traceback for the exception and returns the exception object.
				
					try:
    ...
except SomeException:
    tb = sys.exc_info()[2]
    raise OtherException(...).with_traceback(tb)

				
			

Exception: This is the base class for all non-system-exiting exceptions. User-defined exceptions should inherit from this class.

2. ArithmeticError: Base class for exceptions related to arithmetic operations, such as:

  • OverflowError
  • ZeroDivisionError
  • FloatingPointError

Example:

				
					try:
    a = 10 / 0
    print(a)
except ArithmeticError:
    print("Arithmetic exception occurred.")

				
			

Output:

				
					Arithmetic exception occurred.

				
			

3. BufferError: Raised when an operation related to buffers cannot be completed.

4. LookupError: Base class for exceptions raised when a key or index used on a mapping or sequence is invalid. Examples include:

  • KeyError
  • IndexError

Example:

				
					try:
    a = [1, 2, 3]
    print(a[3])
except LookupError:
    print("Index out of range.")

				
			

Output:

				
					Index out of range.

				
			
Concrete Exceptions

Here are some of the more common exceptions:

  • AssertionError: Raised when an assert statement fails.
				
					class MyClass:
    pass

obj = MyClass()
print(obj.some_attribute)

				
			

Output:

				
					AttributeError: 'MyClass' object has no attribute 'some_attribute'

				
			
  • EOFError: Raised when input() reaches the end of file condition.
				
					while True:
    data = input('Enter name: ')
    print('Hello', data)

				
			

If an EOF is encountered, the interpreter will raise:

				
					EOFError: EOF when reading a line

				
			
  • FloatingPointError: Raised for floating-point operation failures when Python is configured to raise this error.
  • GeneratorExit: Raised when a generator or coroutine is closed.
				
					def generator():
    try:
        yield 1
    except GeneratorExit:
        print('Generator is exiting')

g = generator()
next(g)
g.close()

				
			

Output:

				
					ImportError: No module named 'non_existent_module'

				
			
  • IndexError: Raised for sequence indexes out of range.
				
					lst = [1, 2]
print(lst[3])

				
			

Output:

				
					IndexError: list index out of range

				
			
  • KeyError: Raised when a mapping (dictionary) key is not found
				
					dct = {'a': 1}
print(dct['b'])

				
			

Output:

				
					KeyError: 'b'

				
			
  • KeyboardInterrupt: Raised when the user interrupts execution (e.g., by pressing Ctrl+C).
				
					try:
    input("Press Ctrl+C to interrupt.")
except KeyboardInterrupt:
    print("Execution interrupted by user.")

				
			
  • MemoryError: Raised when an operation runs out of memory.
  • NameError: Raised when a local or global name is not found.
				
					print(unknown_variable)

				
			

Output:

				
					NameError: name 'unknown_variable' is not defined

				
			
  • NotImplementedError: Raised when an abstract method in a user-defined class is not implemented in derived classes.
  • OSError: Raised for operating system-related errors.
  • OverflowError: Raised for arithmetic operations that exceed limits.
  • RecursionError: Raised when the maximum recursion depth is exceeded.
  • ReferenceError: Raised when a weak reference proxy is used to access a referent that has been garbage-collected.
  • RuntimeError: Raised for errors that do not fall into any other category.
  • StopIteration: Raised to indicate the end of an iterator.
  • SyntaxError: Raised when a syntax error is encountered.
				
					eval('x === y')

				
			

Output:

				
					SyntaxError: invalid syntax

				
			
  • SystemError: Raised for internal interpreter errors.
  • SystemExit: Raised by the sys.exit() function.
  • TypeError: Raised when an operation is applied to an object of inappropriate type.
				
					5 + 'a'

				
			

Output:

				
					TypeError: unsupported operand type(s) for +: 'int' and 'str'

				
			
  • UnboundLocalError: A subclass of NameError, raised when a reference is made to a local variable that has not been assigned.
  • UnicodeError: Raised for Unicode-related encoding or decoding errors.
  • ValueError: Raised when a function receives an argument of correct type but invalid value.
				
					int('a')

				
			

Output:

				
					ValueError: invalid literal for int() with base 10: 'a'

				
			
  • ZeroDivisionError: Raised when division or modulo by zero occurs.
				
					print(1 / 0)

				
			

Output:

				
					ZeroDivisionError: division by zero

				
			

In Python, errors can be classified as syntax errors and exceptions. Syntax errors occur due to incorrect syntax, stopping the program’s execution immediately. Exceptions, however, are raised by specific events during execution, changing the program’s normal flow.

Some commonly encountered exceptions include:

  • IOError: Raised when a file operation (such as open) fails.
  • KeyboardInterrupt: Raised when the user interrupts program execution, typically by pressing Ctrl+C.
  • ValueError: Occurs when a function receives an argument of the right type but inappropriate value.
  • EOFError: Raised when the input() function hits an end-of-file condition.
  • ImportError: Raised when an import statement fails to find the specified module.

Try-Except in Python

The try and except statements help manage exceptions in Python. The try block contains the code that may throw an exception, and the except block handles the exception if it occurs.

				
					try:
    # Code to execute
except:
    # Code to execute if an error occurs

				
			
How try Works 

1. Python executes the code within the try block first.
2. If no exception is raised, the except block is skipped.
3. If an exception occurs, Python jumps to the except block.
4. If the exception is not handled, it will propagate to any outer try blocks, and, if unhandled, will stop execution.
5. Multiple except blocks can be used to handle different exceptions.

Example 1: Code runs without any exceptions, so only the try block executes.

				
					def divide(x, y):
    try:
        result = x // y
        print("The result is:", result)
    except ZeroDivisionError:
        print("Error: Division by zero is undefined.")

divide(10, 2)

				
			

Output:

				
					The result is: 5

				
			
Catching Specific Exceptions with as

You can also catch and display the specific type of error that occurs by using Exception as.

				
					def divide(x, y):
    try:
        result = x // y
        print("The result is:", result)
    except Exception as e:
        print("An error occurred:", e)

divide(10, 'A')
divide(10, 0)

				
			

Output:

				
					An error occurred: unsupported operand type(s) for //: 'int' and 'str'
An error occurred: integer division or modulo by zero

				
			
Using else with tryexcept

In Python, you can use an else clause with tryexcept blocks. The else block executes only if no exceptions are raised in the try block.

				
					try:
    # Code to execute
except:
    # Code to execute if an error occurs
else:
    # Code to execute if no exception occurs

				
			

Example:

				
					def safe_divide(a, b):
    try:
        c = (a + b) // (a - b)
    except ZeroDivisionError:
        print("Error: Division by zero.")
    else:
        print("The result is:", c)

safe_divide(5, 3)
safe_divide(3, 3)

				
			

Output:

				
					The result is: 4
Error: Division by zero.

				
			
finally Keyword in Python

The finally block in Python is always executed after the try and except blocks, whether or not an exception occurs. It is often used for cleanup actions.

				
					try:
    # Code to execute
except:
    # Code to execute if an error occurs
else:
    # Code to execute if no exception occurs
finally:
    # Code that always executes

				
			

Example:

				
					try:
    k = 10 // 0  # Raises a ZeroDivisionError
    print(k)
except ZeroDivisionError:
    print("Error: Cannot divide by zero.")
finally:
    print("This block always executes.")

				
			

Output:

				
					Error: Cannot divide by zero.
This block always executes.