Python November 01 ,2025

Exception Handling in Python

 

Table of Contents

  1. What Are Exceptions?
  2. Errors vs. Exceptions
  3. The Exception Hierarchy
  4. Why Handle Exceptions?
  5. The try and except Block
  6. Catching Specific Exceptions
  7. Catching Multiple Exceptions Together
  8. Using else in Exception Handling
  9. The finally Block
  10. The Complete try-except-else-finally Structure
  11. The raise Statement
  12. The assert Statement
  13. Creating Custom Exceptions
  14. Nested try-except Blocks
  15. Exception Propagation
  16. Catching All Exceptions (with care)
  17. Practical Example: File Handling with Exceptions
  18. Summary Table
  19. Real-Life Use Cases
  20. Best Practices

 

1. What Are Exceptions?

An exception in Python is an event (or signal) that occurs during the execution of a program and disrupts its normal flow.

When Python encounters an error that it cannot handle at runtime — like dividing by zero, accessing an invalid index, or converting a string to an integer — it raises an exception.

For example:

a = 10
b = 0
print(a / b)

Output:

ZeroDivisionError: division by zero

The line a / b triggers a runtime error, and Python immediately:

  1. Creates an exception object (ZeroDivisionError).
  2. Stops executing the current block.
  3. Looks for code that can handle this exception.

If it finds no handler, the program terminates abnormally.

2. Errors vs. Exceptions

  • Errors: Problems in the code that cannot be handled at runtime.

    • Example: SyntaxError, IndentationError
    • These occur before the code executes — the interpreter stops immediately.
    print("Hello"
    # SyntaxError: missing closing parenthesis
    
  • Exceptions: Problems that occur during execution and can be handled by the programmer.

    • Example: ZeroDivisionError, FileNotFoundError
    a = 10 / 0   # Exception occurs here
    

In short:
Errors → Detected during compilation (syntax checking).
Exceptions → Detected during runtime (execution).

3. The Exception Hierarchy

All exceptions in Python inherit from the BaseException class.

The hierarchy looks like this (simplified):

BaseException
 ├── SystemExit
 ├── KeyboardInterrupt
 ├── Exception
      ├── ArithmeticError
      │     ├── ZeroDivisionError
      │     ├── OverflowError
      │     └── FloatingPointError
      ├── ImportError
      │     ├── ModuleNotFoundError
      ├── IndexError
      ├── KeyError
      ├── ValueError
      ├── TypeError
      ├── FileNotFoundError
      └── OSError
  • The Exception class is the base for all user-level exceptions.
  • The BaseException class also includes system-level exceptions like KeyboardInterrupt (Ctrl+C).

4. Why Handle Exceptions?

Without exception handling, a single runtime error can crash the entire program.

Example:

a = int(input("Enter a number: "))
print(10 / a)
print("End of program")

If you enter 0, you’ll get:

ZeroDivisionError: division by zero

The last print statement is never executed.

To prevent this, we use exception handling so that:

  • The program does not crash.
  • We can show a user-friendly message.
  • We can continue execution safely.

5. The try and except Block

The simplest form of exception handling in Python:

try:
    # Code that might cause an exception
    x = 10 / 0
except:
    # Code that runs if an exception occurs
    print("An error occurred")

Output:

An error occurred

How It Works:

  1. Python executes the code inside the try block.
  2. If no error → skips the except block.
  3. If an error occurs → jumps immediately to the except block.
  4. The program continues after the except block.

6. Catching Specific Exceptions

Catching all exceptions using a broad except: is generally not recommended, because it hides real errors.
Instead, we catch specific exception types.

Example:

try:
    num = int(input("Enter a number: "))
    print(10 / num)

except ValueError:
    print("Please enter a valid number.")

except ZeroDivisionError:
    print("Cannot divide by zero.")

Explanation:

  • If the input is "abc" → ValueError
  • If the input is 0 → ZeroDivisionError
  • Python checks each except block from top to bottom and runs the first matching one.

Catching All Exceptions (with care)

Sometimes you want to catch any error, such as in:

  • Logging systems
  • Large applications where you don’t want one minor error to crash the program
  • Situations where you must ensure cleanup happens

In such cases, Python allows a generic exception handler, but it must be used carefully.

Using a Generic except: (Not Recommended)

try:
    risky_code()
except:
    print("Something went wrong.")

Why this is dangerous?

  • It catches everything, including system-level exceptions like:
    • KeyboardInterrupt
    • SystemExit
    • MemoryError

These should not be suppressed.

This makes debugging extremely difficult because you hide the real cause.

Safer Approach — Catch Exception Explicitly

try:
    risky_code()
except Exception as e:
    print("An error occurred:", e)

Why this is better:

  • It catches only errors that inherit from the base Exception class
  • It does NOT catch system-exiting exceptions, which should be allowed to pass through

Useful when:

  • You want error handling but still want meaningful debugging
  • You want to log the exact exception message
  • You want the program to continue without crashing

 Logging Instead of Silently Hiding

A more professional approach:

import logging

try:
    risky_code()
except Exception as e:
    logging.error("Error occurred: %s", e)

Benefits:

  • You don’t hide the error
  • You keep a record
  • Program continues safely

 Combining Specific and Generic Exceptions

Best practice:

try:
    num = int(input("Enter a number: "))
    print(10 / num)

except ValueError:
    print("Invalid number!")

except ZeroDivisionError:
    print("Cannot divide by zero.")

except Exception as e:
    print("Unexpected error occurred:", e)

Why this is ideal?

  • Handles expected errors first
  • Catches any unexpected errors last
  • Still provides debugging info (e)
  • Avoids suppressing all errors silently

Python checks each except clause in order, and executes the first one that matches.

7. Catching Multiple Exceptions Together

Sometimes, you want to handle multiple exceptions the same way:

try:
    result = 10 / int(input("Enter a number: "))
except (ValueError, ZeroDivisionError) as e:
    print("Error:", e)

Explanation:

  • (ValueError, ZeroDivisionError) groups exceptions.
  • as e captures the exception object for detailed info.

8. Using else in Exception Handling

else is optional — it runs only if no exception occurs.

try:
    num = int(input("Enter number: "))
except ValueError:
    print("Invalid input.")
else:
    print("You entered:", num)

Flow:

  • If input is invalid → except runs.
  • If input is valid → else runs.

This is useful for separating error-handling and successful logic clearly.

9. The finally Block

The finally block in Python is a special part of the error-handling structure that always executes, no matter what happens inside the try or except blocks.

Whether:

  • No error occurs
  • An exception is raised
  • The exception is caught
  • The exception is not caught
  • A return statement is triggered
  • A loop is broken
  • The program exits the try block early

➡️ The finally block will still run.

Why is finally important?

It is mainly used for clean-up tasks, such as:

  • Closing files
  • Closing database connections
  • Releasing network sockets
  • Releasing locks
  • Cleaning temporary resources
  • Stopping background processes

Resources must be released whether the code succeeds or fails, which makes finally crucial.

Example: File Handling

try:
    file = open('data.txt', 'r')
    data = file.read()

except FileNotFoundError:
    print("File not found.")

finally:
    file.close()
    print("File closed.")

Explanation:

  • If file exists → file is read, then closed
  • If file does not exist → FileNotFoundError occurs, message prints, then file would still try to close
  • Regardless of success or failure → finally executes

Important Note:

If the file fails to open (FileNotFoundError), file may not exist → causing a second error.

A safer version:

file = None

try:
    file = open('data.txt', 'r')
    data = file.read()

except FileNotFoundError:
    print("File not found.")

finally:
    if file is not None:
        file.close()
        print("File closed.")

finally Runs Even With return

def test():
    try:
        return "Try block return"
    finally:
        print("Finally executed")

print(test())

Output:

Finally executed
Try block return

Even though the function returns early, the finally block still runs.

finally Runs Even If Exception Is Not Caught

try:
    x = 1 / 0
finally:
    print("This still runs!")

Output:

This still runs!
ZeroDivisionError: division by zero

The finally executes before the program crashes.

try-except-finally Full Structure

try:
    # Code that may raise errors
except SomeError:
    # Handle that specific error
except OtherError:
    # Handle another error
else:
    # Runs if no exception happens
finally:
    # Always runs (cleanup)

Real-World Uses of finally

✔ Closing files

✔ Closing database connections

finally:
    db.close()

✔ Releasing network sockets

finally:
    sock.close()

✔ Stopping background threads

finally:
    thread.stop()

✔ Releasing locks in multithreading

finally:
    lock.release()

✔ Ensuring temporary files get deleted

finally:
    os.remove(temp_file)

10. The Complete try-except-else-finally Structure

try:
    x = int(input("Enter a number: "))
    result = 10 / x
except ZeroDivisionError:
    print("Cannot divide by zero.")
except ValueError:
    print("Invalid input.")
else:
    print("Result:", result)
finally:
    print("Execution complete.")

Flow:

  1. try runs first.
  2. If exception → corresponding except executes.
  3. If no exception → else executes.
  4. finally always executes.

11. The raise Statement (Manually Raising Exceptions)

You can manually trigger an exception using raise.

Useful when you want to enforce conditions or validations in your own code.

x = -1
if x < 0:
    raise ValueError("x cannot be negative")

Output:

ValueError: x cannot be negative

You can also raise built-in or custom exceptions dynamically:

try:
    raise ZeroDivisionError("Custom divide error")
except ZeroDivisionError as e:
    print("Caught:", e)

12. The assert Statement —

What It Is:

The assert statement in Python is a debugging and testing tool used to check if a certain condition is True during program execution.
If the condition is True, the program continues normally.
If the condition is False, an AssertionError is raised — optionally with a custom error message.

In simple terms, assert helps you verify that your assumptions about the code are correct while the program runs.

Syntax:

assert condition, optional_message
  • condition → The logical expression you want to test.
  • optional_message → (Optional) A message that appears if the assertion fails.

Example 1: Basic Assertion

x = 10
assert x > 5
print("Assertion passed!")

Output:

Assertion passed!

Explanation:
x > 5 is True, so the program continues without any error.

Example 2: Failed Assertion with Message

x = 10
assert x < 5, "x must be less than 5"

Output:

AssertionError: x must be less than 5

Explanation:
x < 5 is False, so Python raises an AssertionError and displays the message "x must be less than 5".

Why It Is Used:

1. Debugging Aid:
Used by developers to detect logic errors early in the development process.

2. Validation:
Ensures that variables, inputs, or outputs meet expected conditions before proceeding.

3. Testing:
Used in unit testing to verify function outputs or program state.

4. Preventing Invalid States:
Stops program execution immediately if something unexpected occurs.

Example 3: Using assert in a Function

def divide(a, b):
    assert b != 0, "Denominator must not be zero"
    return a / b

print(divide(10, 2))
print(divide(5, 0))

Output:

5.0
AssertionError: Denominator must not be zero

Explanation:
The assertion ensures that the denominator (b) is not zero before division.
If it is zero, the program stops and raises an error instead of crashing later.

Important Notes:

⚙️ Assertions can be disabled when running Python in optimized mode (using the -O flag):

python -O myfile.py

In this case, all assert statements are ignored.
Hence, assert should not be used for runtime input validation — only for debugging and testing.

 

13. Creating Custom Exceptions

You can define your own exception types by creating a new class that inherits from Exception.

class NegativeNumberError(Exception):
    """Custom exception for negative numbers"""
    pass

def check_positive(num):
    if num < 0:
        raise NegativeNumberError("Negative numbers not allowed")
    return num

try:
    check_positive(-5)
except NegativeNumberError as e:
    print("Error:", e)

Explanation:

  • You define a new exception class.
  • Raise it when needed using raise.
  • Catch it like any other exception.

Custom exceptions improve readability and debugging for large applications.

14. Nested try-except Blocks

You can nest one try-except inside another — useful for handling multiple levels of operations.

try:
    try:
        x = int(input("Enter number: "))
        y = 10 / x
    except ZeroDivisionError:
        print("Inner exception: divide by zero")
except Exception as e:
    print("Outer exception:", e)

If an exception isn’t caught inside the inner block, it propagates to the outer block.

15. Exception Propagation

Exception Propagation refers to the process by which an exception (error) moves up through the call stack when it is not handled in the place where it occurs.When an exception occurs:

  1. Python searches for a matching except in the current block.
  2. If not found, it moves one level up (caller function).
  3. If it still doesn’t find one, the program terminates.

Example:

def inner():
    print(10 / 0)

def outer():
    inner()

try:
    outer()
except ZeroDivisionError:
    print("Caught in main")

Output:

Caught in main

The exception propagates from inner() → outer() → main program.

16. Catching All Exceptions (with care)

You can catch any exception using Exception class.

try:
    risky_operation()
except Exception as e:
    print("Unexpected error:", e)

⚠️ But be cautious!

  • It can hide real programming bugs.
  • Use it only when you truly don’t know what to expect (like in system scripts).

17. Practical Example: File Handling with Exceptions

try:
    filename = input("Enter file name: ")
    with open(filename, 'r') as f:
        data = f.read()
except FileNotFoundError:
    print("The file doesn't exist.")
except PermissionError:
    print("You don't have permission to read this file.")
else:
    print("File read successfully.")
finally:
    print("Operation complete.")

This is how real programs safely interact with external resources.

18. Summary Table

KeywordDescription
tryDefines code that might raise an exception
exceptDefines code that runs if an exception occurs
elseRuns if no exception occurs
finallyRuns always, used for cleanup
raiseManually trigger an exception
assertDebugging check that raises AssertionError if false

19. Real-Life Use Cases

  • Handling user input errors.
  • Managing file operations safely.
  • Preventing crashes during network or database access.
  • Building robust APIs and web applications.
  • Logging unexpected errors in production systems.

20. Best Practices

  1. Always handle specific exceptions first.
  2. Don’t use a bare except: (it hides all errors).
  3. Use finally for cleanup (like closing files or sockets).
  4. Use custom exceptions to represent meaningful domain errors.
  5. Keep try blocks small and focused.
  6. Avoid overusing exception handling for flow control — they’re meant for exceptional situations, not logic.

     

Next Blog- Object-Oriented Programming (OOP) in Python

 

Sanjiv
0

You must logged in to post comments.

Get In Touch

G06, Kristal Olivine Bellandur near Bangalore Central Mall, Bangalore Karnataka, 560103

+91-8076082435

techiefreak87@gmail.com