Python Error Handling
Error handling allows your program to gracefully handle unexpected situations instead of crashing. Python uses exceptions — objects that represent errors — and the try-except mechanism to catch and handle them.
Types of Errors
Syntax Errors (Caught Before Execution)
python
# Missing colon
# if True
# print("error") # SyntaxError
# Unclosed bracket
# data = [1, 2, 3 # SyntaxErrorExceptions (Caught During Execution)
python
# Common exceptions
print(10 / 0) # ZeroDivisionError
print(int("abc")) # ValueError
print([1, 2][5]) # IndexError
print({"a": 1}["b"]) # KeyError
print(undefined_var) # NameError
print("hello" + 5) # TypeErrortry-except
python
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero!")
# Program continues...
print("Still running!")Catch Specific Exceptions
python
try:
number = int(input("Enter a number: "))
result = 100 / number
print(f"Result: {result}")
except ValueError:
print("That's not a valid number!")
except ZeroDivisionError:
print("Cannot divide by zero!")Catch Multiple Exceptions
python
try:
value = int("abc")
except (ValueError, TypeError) as e:
print(f"Error: {e}")Catch All Exceptions
python
try:
risky_operation()
except Exception as e:
print(f"Something went wrong: {type(e).__name__}: {e}")
# Bare except (catches EVERYTHING - avoid this)
try:
risky_operation()
except: # Not recommended - catches KeyboardInterrupt, SystemExit, etc.
print("Error occurred")Best Practice: Always catch specific exceptions. Avoid bare except: clauses.
try-except-else-finally
python
try:
number = int(input("Enter a number: "))
result = 100 / number
except ValueError:
print("Invalid input!")
except ZeroDivisionError:
print("Cannot divide by zero!")
else:
# Runs only if NO exception occurred
print(f"Result: {result}")
finally:
# ALWAYS runs, whether exception occurred or not
print("Execution complete")finally for Cleanup
python
file = None
try:
file = open("data.txt", "r")
content = file.read()
process(content)
except FileNotFoundError:
print("File not found!")
except Exception as e:
print(f"Error: {e}")
finally:
if file:
file.close() # Always close the file
print("Cleanup done")
# Better: use context manager
with open("data.txt", "r") as file:
content = file.read()
# Automatically closed, even on errorRaising Exceptions
Use raise to throw an exception:
python
def set_age(age):
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age cannot exceed 150")
return age
try:
set_age(-5)
except ValueError as e:
print(f"Invalid age: {e}")
# Invalid age: Age cannot be negative
# Re-raise an exception
try:
set_age("abc")
except TypeError:
print("Logging error...")
raise # Re-raises the same exceptionCustom Exceptions
Create your own exception classes by inheriting from Exception:
python
class AppError(Exception):
"""Base exception for the application."""
pass
class ValidationError(AppError):
"""Raised when validation fails."""
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"Validation error on '{field}': {message}")
class AuthenticationError(AppError):
"""Raised when authentication fails."""
def __init__(self, username):
self.username = username
super().__init__(f"Authentication failed for user '{username}'")
class NotFoundError(AppError):
"""Raised when a resource is not found."""
pass
# Usage
def login(username, password):
if not username:
raise ValidationError("username", "cannot be empty")
if len(password) < 8:
raise ValidationError("password", "must be at least 8 characters")
if username != "admin" or password != "secret123":
raise AuthenticationError(username)
return {"username": username, "role": "admin"}
try:
user = login("admin", "short")
except ValidationError as e:
print(f"Validation: {e.field} - {e.message}")
except AuthenticationError as e:
print(f"Auth failed: {e.username}")
except AppError as e:
print(f"App error: {e}")Exception Hierarchy
Python's built-in exception hierarchy:
BaseException
├── KeyboardInterrupt
├── SystemExit
├── GeneratorExit
└── Exception
├── ArithmeticError
│ ├── ZeroDivisionError
│ ├── OverflowError
│ └── FloatingPointError
├── AttributeError
├── ImportError
│ └── ModuleNotFoundError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── NameError
├── OSError
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── FileExistsError
├── TypeError
├── ValueError
└── RuntimeError
└── RecursionErrorCommon Exception Patterns
LBYL vs EAFP
python
# LBYL: Look Before You Leap (check first)
if key in dictionary:
value = dictionary[key]
else:
value = default
# EAFP: Easier to Ask Forgiveness than Permission (try first)
try:
value = dictionary[key]
except KeyError:
value = default
# Python community prefers EAFPRetry Pattern
python
def retry(func, max_attempts=3):
"""Retry a function up to max_attempts times."""
for attempt in range(1, max_attempts + 1):
try:
return func()
except Exception as e:
if attempt == max_attempts:
raise
print(f"Attempt {attempt} failed: {e}")Exception Chaining
python
try:
data = {"key": "not_a_number"}
value = int(data["key"])
except ValueError as e:
raise RuntimeError("Failed to process data") from e
# RuntimeError: Failed to process data
# The above exception was the direct cause of:
# ValueError: invalid literal for int()Practical Example: Safe Data Processor
python
"""
A robust data processor with proper error handling.
"""
class DataProcessor:
def __init__(self):
self.errors = []
def process_record(self, record):
"""Process a single record with error handling."""
try:
name = record["name"]
age = int(record["age"])
score = float(record["score"])
if age < 0 or age > 150:
raise ValueError(f"Invalid age: {age}")
if score < 0 or score > 100:
raise ValueError(f"Invalid score: {score}")
return {
"name": name.title(),
"age": age,
"score": score,
"grade": self._calculate_grade(score)
}
except KeyError as e:
self.errors.append(f"Missing field {e} in record: {record}")
except (ValueError, TypeError) as e:
self.errors.append(f"Invalid data in record {record}: {e}")
return None
def _calculate_grade(self, score):
if score >= 90: return "A"
if score >= 80: return "B"
if score >= 70: return "C"
if score >= 60: return "D"
return "F"
def process_all(self, records):
"""Process all records, collecting results and errors."""
results = []
for record in records:
result = self.process_record(record)
if result:
results.append(result)
return results
# Usage
processor = DataProcessor()
raw_data = [
{"name": "alice", "age": "25", "score": "92"},
{"name": "bob", "age": "bad", "score": "85"}, # ValueError
{"name": "charlie", "score": "78"}, # Missing age
{"name": "diana", "age": "22", "score": "95"},
{"name": "eve", "age": "30", "score": "200"}, # Invalid score
]
results = processor.process_all(raw_data)
print("Processed Records:")
for r in results:
print(f" {r['name']}: Grade {r['grade']} (score: {r['score']})")
print(f"\nErrors ({len(processor.errors)}):")
for error in processor.errors:
print(f" ⚠ {error}")Summary
- Use
try-exceptto catch and handle exceptions gracefully - Catch specific exceptions — avoid bare
except:clauses elseruns when no exception occurs;finallyalways runsraiseto throw exceptions;raise ... fromfor exception chaining- Create custom exceptions by inheriting from
Exception - Python prefers EAFP (try first) over LBYL (check first)
- Use context managers (
with) for automatic resource cleanup - Always handle exceptions at the appropriate level of your application
Next, we'll learn about Python generators and iterators.