Python Exceptions
Errors vs Bugs vs Exceptions
Syntax Errors
Errors in your code before it runs. Python can't even understand what you wrote.
# Missing colon
if True
print("Hello") # SyntaxError: expected ':'
# Unclosed parenthesis
print("Hello" # SyntaxError: '(' was never closed
Fix: Correct the syntax. Python tells you exactly where the problem is.
Bugs
Your code runs, but it does the wrong thing. No error message - just incorrect behavior.
# Bug: wrong formula
def circle_area(radius):
return 2 * 3.14 * radius # Wrong! This is circumference, not area
print(circle_area(5)) # Returns 31.4, should be 78.5
Why "bug"? Legend says early computers had actual insects causing problems. The term stuck.
Fix: Debug your code - find and fix the logic error.
Exceptions
Errors that occur during execution. The code is syntactically correct, but something goes wrong at runtime.
# Runs fine until...
x = 10 / 0 # ZeroDivisionError: division by zero
# Or...
my_list = [1, 2, 3]
print(my_list[10]) # IndexError: list index out of range
Fix: Handle the exception or prevent the error condition.
What is an Exception?
An exception is Python's way of saying "something unexpected happened and I can't continue."
When an exception occurs:
- Python stops normal execution
- Creates an exception object with error details
- Looks for code to handle it
- If no handler found, program crashes with traceback
# Exception in action
print("Start")
x = 10 / 0 # Exception here!
print("End") # Never reached
# Output:
# Start
# Traceback (most recent call last):
# File "example.py", line 2, in <module>
# x = 10 / 0
# ZeroDivisionError: division by zero
Common Exceptions
# ZeroDivisionError
10 / 0
# TypeError - wrong type
"hello" + 5
# ValueError - right type, wrong value
int("hello")
# IndexError - list index out of range
[1, 2, 3][10]
# KeyError - dictionary key not found
{'a': 1}['b']
# FileNotFoundError
open("nonexistent.txt")
# AttributeError - object has no attribute
"hello".append("!")
# NameError - variable not defined
print(undefined_variable)
# ImportError - module not found
import nonexistent_module
Handling Exceptions
Basic try/except
try:
x = 10 / 0
except:
print("Something went wrong!")
# Output: Something went wrong!
Problem: This catches ALL exceptions - even ones you didn't expect. Not recommended.
Catching Specific Exceptions (Recommended)
try:
x = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero!")
# Output: Cannot divide by zero!
Catching Multiple Specific Exceptions
try:
value = int(input("Enter a number: "))
result = 10 / value
except ValueError:
print("That's not a valid number!")
except ZeroDivisionError:
print("Cannot divide by zero!")
Catching Multiple Exceptions Together
try:
# Some risky code
pass
except (ValueError, TypeError):
print("Value or Type error occurred!")
Getting Exception Details
try:
x = 10 / 0
except ZeroDivisionError as e:
print(f"Error: {e}")
print(f"Type: {type(e).__name__}")
# Output:
# Error: division by zero
# Type: ZeroDivisionError
The Complete try/except/else/finally
try:
# Code that might raise an exception
result = 10 / 2
except ZeroDivisionError:
# Runs if exception occurs
print("Cannot divide by zero!")
else:
# Runs if NO exception occurs
print(f"Result: {result}")
finally:
# ALWAYS runs, exception or not
print("Cleanup complete")
# Output:
# Result: 5.0
# Cleanup complete
When to Use Each Part
| Block | When It Runs | Use For |
|---|---|---|
try | Always attempts | Code that might fail |
except | If exception occurs | Handle the error |
else | If NO exception | Code that depends on try success |
finally | ALWAYS | Cleanup (close files, connections) |
finally is Guaranteed
def risky_function():
try:
return 10 / 0
except ZeroDivisionError:
return "Error!"
finally:
print("This ALWAYS prints!")
result = risky_function()
# Output: This ALWAYS prints!
# result = "Error!"
Best Practices
1. Be Specific - Don't Catch Everything
# BAD - catches everything, hides bugs
try:
do_something()
except:
pass
# GOOD - catches only what you expect
try:
do_something()
except ValueError:
handle_value_error()
2. Don't Silence Exceptions Without Reason
# BAD - silently ignores errors
try:
important_operation()
except Exception:
pass # What went wrong? We'll never know!
# GOOD - at least log it
try:
important_operation()
except Exception as e:
print(f"Error occurred: {e}")
# or use logging.error(e)
3. Use else for Code That Depends on try Success
# Less clear
try:
file = open("data.txt")
content = file.read()
process(content)
except FileNotFoundError:
print("File not found")
# More clear - separate "risky" from "safe" code
try:
file = open("data.txt")
except FileNotFoundError:
print("File not found")
else:
content = file.read()
process(content)
4. Use finally for Cleanup
file = None
try:
file = open("data.txt")
content = file.read()
except FileNotFoundError:
print("File not found")
finally:
if file:
file.close() # Always close, even if error
# Even better - use context manager
with open("data.txt") as file:
content = file.read() # Automatically closes!
5. Catch Exceptions at the Right Level
# Don't catch too early
def read_config():
# Let the caller handle missing file
with open("config.txt") as f:
return f.read()
# Catch at appropriate level
def main():
try:
config = read_config()
except FileNotFoundError:
print("Config file missing, using defaults")
config = get_defaults()
Raising Exceptions
Use raise to throw your own exceptions:
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero!")
return a / b
try:
result = divide(10, 0)
except ValueError as e:
print(e) # Cannot divide by zero!
Re-raising Exceptions
try:
risky_operation()
except ValueError:
print("Logging this error...")
raise # Re-raise the same exception
Built-in Exception Hierarchy
All exceptions inherit from BaseException. Here's the hierarchy:
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
├── StopIteration
├── ArithmeticError
│ ├── FloatingPointError
│ ├── OverflowError
│ └── ZeroDivisionError
├── AssertionError
├── AttributeError
├── BufferError
├── EOFError
├── ImportError
│ └── ModuleNotFoundError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── MemoryError
├── NameError
│ └── UnboundLocalError
├── OSError
│ ├── FileExistsError
│ ├── FileNotFoundError
│ ├── IsADirectoryError
│ ├── NotADirectoryError
│ ├── PermissionError
│ └── TimeoutError
├── ReferenceError
├── RuntimeError
│ ├── NotImplementedError
│ └── RecursionError
├── SyntaxError
│ └── IndentationError
│ └── TabError
├── TypeError
└── ValueError
└── UnicodeError
├── UnicodeDecodeError
├── UnicodeEncodeError
└── UnicodeTranslateError
Why Hierarchy Matters
Catching a parent catches all children:
# Catches ZeroDivisionError, OverflowError, FloatingPointError
try:
result = 10 / 0
except ArithmeticError:
print("Math error!")
# Catches IndexError and KeyError
try:
my_list[100]
except LookupError:
print("Lookup failed!")
Tip: Catch Exception instead of bare except: - it doesn't catch KeyboardInterrupt or SystemExit.
# Better than bare except
try:
do_something()
except Exception as e:
print(f"Error: {e}")
User-Defined Exceptions
Create custom exceptions by inheriting from Exception:
Basic Custom Exception
class InvalidDNAError(Exception):
"""Raised when DNA sequence contains invalid characters"""
pass
def validate_dna(sequence):
valid_bases = set("ATGC")
for base in sequence.upper():
if base not in valid_bases:
raise InvalidDNAError(f"Invalid base: {base}")
return True
try:
validate_dna("ATGXCCC")
except InvalidDNAError as e:
print(f"Invalid DNA: {e}")
Custom Exception with Attributes
class InsufficientFundsError(Exception):
"""Raised when account has insufficient funds"""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
self.shortage = amount - balance
super().__init__(
f"Cannot withdraw ${amount}. "
f"Balance: ${balance}. "
f"Short by: ${self.shortage}"
)
class BankAccount:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
return amount
# Usage
account = BankAccount(100)
try:
account.withdraw(150)
except InsufficientFundsError as e:
print(e)
print(f"You need ${e.shortage} more")
# Output:
# Cannot withdraw $150. Balance: $100. Short by: $50
# You need $50 more
Exception Hierarchy for Your Project
# Base exception for your application
class BioinformaticsError(Exception):
"""Base exception for bioinformatics operations"""
pass
# Specific exceptions
class SequenceError(BioinformaticsError):
"""Base for sequence-related errors"""
pass
class InvalidDNAError(SequenceError):
"""Invalid DNA sequence"""
pass
class InvalidRNAError(SequenceError):
"""Invalid RNA sequence"""
pass
class AlignmentError(BioinformaticsError):
"""Sequence alignment failed"""
pass
# Now you can catch at different levels
try:
process_sequence()
except InvalidDNAError:
print("DNA issue")
except SequenceError:
print("Some sequence issue")
except BioinformaticsError:
print("General bioinformatics error")
Exercises
Exercise 1: Write code that catches a ZeroDivisionError and prints a friendly message.
Exercise 2: Ask user for a number, handle both ValueError (not a number) and ZeroDivisionError (if dividing by it).
Exercise 3: Write a function that opens a file and handles FileNotFoundError.
Exercise 4: Create a function that takes a list and index, returns the element, handles IndexError.
Exercise 5: Write code that handles KeyError when accessing a dictionary.
Exercise 6: Create a custom NegativeNumberError and raise it if a number is negative.
Exercise 7: Write a function that converts string to int, handling ValueError, and returns 0 on failure.
Exercise 8: Use try/except/else/finally to read a file and ensure it's always closed.
Exercise 9: Create a custom InvalidAgeError with min and max age attributes.
Exercise 10: Write a function that validates an email (must contain @), raise ValueError if invalid.
Exercise 11: Handle multiple exceptions: TypeError, ValueError, ZeroDivisionError in one block.
Exercise 12: Create a hierarchy: ValidationError → EmailError, PhoneError.
Exercise 13: Re-raise an exception after logging it.
Exercise 14: Create a InvalidSequenceError for DNA validation with the invalid character as attribute.
Exercise 15: Write a "safe divide" function that returns None on any error instead of crashing.
Solutions
# Exercise 1
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero!")
# Exercise 2
try:
num = int(input("Enter a number: "))
result = 100 / num
print(f"100 / {num} = {result}")
except ValueError:
print("That's not a valid number!")
except ZeroDivisionError:
print("Cannot divide by zero!")
# Exercise 3
def read_file(filename):
try:
with open(filename) as f:
return f.read()
except FileNotFoundError:
print(f"File '{filename}' not found")
return None
# Exercise 4
def safe_get(lst, index):
try:
return lst[index]
except IndexError:
print(f"Index {index} out of range")
return None
# Exercise 5
d = {'a': 1, 'b': 2}
try:
value = d['c']
except KeyError:
print("Key not found!")
value = None
# Exercise 6
class NegativeNumberError(Exception):
pass
def check_positive(n):
if n < 0:
raise NegativeNumberError(f"{n} is negative!")
return n
# Exercise 7
def safe_int(s):
try:
return int(s)
except ValueError:
return 0
# Exercise 8
file = None
try:
file = open("data.txt")
content = file.read()
except FileNotFoundError:
print("File not found")
content = ""
else:
print("File read successfully")
finally:
if file:
file.close()
print("Cleanup done")
# Exercise 9
class InvalidAgeError(Exception):
def __init__(self, age, min_age=0, max_age=150):
self.age = age
self.min_age = min_age
self.max_age = max_age
super().__init__(f"Age {age} not in range [{min_age}, {max_age}]")
# Exercise 10
def validate_email(email):
if '@' not in email:
raise ValueError(f"Invalid email: {email} (missing @)")
return True
# Exercise 11
try:
# risky code
pass
except (TypeError, ValueError, ZeroDivisionError) as e:
print(f"Error: {e}")
# Exercise 12
class ValidationError(Exception):
pass
class EmailError(ValidationError):
pass
class PhoneError(ValidationError):
pass
# Exercise 13
try:
result = 10 / 0
except ZeroDivisionError:
print("Logging: Division by zero occurred")
raise
# Exercise 14
class InvalidSequenceError(Exception):
def __init__(self, sequence, invalid_char):
self.sequence = sequence
self.invalid_char = invalid_char
super().__init__(f"Invalid character '{invalid_char}' in sequence")
def validate_dna(seq):
for char in seq:
if char not in "ATGC":
raise InvalidSequenceError(seq, char)
return True
# Exercise 15
def safe_divide(a, b):
try:
return a / b
except Exception:
return None
print(safe_divide(10, 2)) # 5.0
print(safe_divide(10, 0)) # None
print(safe_divide("a", 2)) # None
Summary
| Concept | Description |
|---|---|
| Syntax Error | Code is malformed, won't run |
| Bug | Code runs but gives wrong result |
| Exception | Runtime error, can be handled |
| try/except | Catch and handle exceptions |
| else | Runs if no exception |
| finally | Always runs (cleanup) |
| raise | Throw an exception |
| Custom Exception | Inherit from Exception |
Best Practices:
- Catch specific exceptions, not bare
except: - Don't silence exceptions without reason
- Use
finallyfor cleanup - Create custom exceptions for your domain
- Build exception hierarchies for complex projects