Exception Handling in Python
Table of Contents
- What Are Exceptions?
- Errors vs. Exceptions
- The Exception Hierarchy
- Why Handle Exceptions?
- The try and except Block
- Catching Specific Exceptions
- Catching Multiple Exceptions Together
- Using else in Exception Handling
- The finally Block
- The Complete try-except-else-finally Structure
- The raise Statement
- The assert Statement
- Creating Custom Exceptions
- Nested try-except Blocks
- Exception Propagation
- Catching All Exceptions (with care)
- Practical Example: File Handling with Exceptions
- Summary Table
- Real-Life Use Cases
- 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:
- Creates an exception object (ZeroDivisionError).
- Stops executing the current block.
- 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 parenthesisExceptions: 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:
- Python executes the code inside the try block.
- If no error → skips the except block.
- If an error occurs → jumps immediately to the except block.
- 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:
- try runs first.
- If exception → corresponding except executes.
- If no exception → else executes.
- 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:
- Python searches for a matching except in the current block.
- If not found, it moves one level up (caller function).
- 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
| Keyword | Description |
|---|---|
| try | Defines code that might raise an exception |
| except | Defines code that runs if an exception occurs |
| else | Runs if no exception occurs |
| finally | Runs always, used for cleanup |
| raise | Manually trigger an exception |
| assert | Debugging 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
- Always handle specific exceptions first.
- Don’t use a bare except: (it hides all errors).
- Use finally for cleanup (like closing files or sockets).
- Use custom exceptions to represent meaningful domain errors.
- Keep try blocks small and focused.
Avoid overusing exception handling for flow control — they’re meant for exceptional situations, not logic.
Next Blog- Object-Oriented Programming (OOP) in Python
