Error Handling
Real programs fail — bad input, missing files, network timeouts. Learn to anticipate failures and handle them gracefully so your code never just crashes on the user.
Try / Except
Wrap risky code in a try block. If an exception is raised, Python jumps to the matching except block instead of crashing.
# Without error handling — crashes if input is "hello"
# number = int(input("Enter a number: ")) # ValueError!
# With error handling
def safe_divide(a, b):
try:
result = a / b
return result
except ZeroDivisionError:
print("Error: cannot divide by zero")
return None
except TypeError as e:
print(f"Error: wrong type — {e}")
return None
print(safe_divide(10, 2)) # 5.0
print(safe_divide(10, 0)) # Error: cannot divide by zero → None
print(safe_divide(10, "x")) # Error: wrong type → None
# Catch multiple types in one line
try:
x = int("not a number")
except (ValueError, TypeError) as err:
print(f"Conversion failed: {err}")
Output
Never use bare
except:Always catch specific exceptions. A bare except: silently catches everything — including keyboard interrupts and system exit — which makes debugging a nightmare.Else and Finally
def read_number(text: str):
try:
result = int(text)
except ValueError:
print(f" '{text}' is not a valid integer")
return None
else:
# Runs ONLY if no exception occurred
print(f" Successfully parsed: {result}")
return result
finally:
# ALWAYS runs — exception or not
# Great for cleanup: close files, release locks, etc.
print(f" (attempted to parse '{text}')")
read_number("42")
print("---")
read_number("hello")
print("---")
# Real-world pattern: file handling
def read_file_safe(path: str):
try:
with open(path, "r") as f:
return f.read()
except FileNotFoundError:
print(f"File not found: {path}")
return ""
except PermissionError:
print(f"Permission denied: {path}")
return ""
Output
Raising Exceptions
You can raise exceptions yourself with raise. This signals to calling code that something went wrong.
# Custom exception classes
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
super().__init__(f"Need ${amount:.2f} but only have ${balance:.2f}")
self.balance = balance
self.amount = amount
class NegativeAmountError(ValueError):
pass
class BankAccount:
def __init__(self, balance=0.0):
self.balance = float(balance)
def withdraw(self, amount: float):
if amount <= 0:
raise NegativeAmountError(f"Amount must be positive, got {amount}")
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
return self.balance
acc = BankAccount(100)
try:
acc.withdraw(150)
except InsufficientFundsError as e:
print(f"Failed: {e}")
except NegativeAmountError as e:
print(f"Bad input: {e}")
print(f"Balance unchanged: ${acc.balance}")
Output
Common Built-in Exceptions
| Exception | When it occurs |
|---|---|
| ValueError | Right type, wrong value — int("hello") |
| TypeError | Wrong type — "hi" + 5 |
| KeyError | Key not in dict — d["missing"] |
| IndexError | Index out of range — lst[99] |
| AttributeError | Object doesn't have that attribute/method |
| FileNotFoundError | File or directory not found |
| ZeroDivisionError | Dividing by zero |
| ImportError | Module not found or can't be imported |