try / except
Use try/except to handle errors without crashing. Catch specific exceptions - never use a bare except:.
# Basic try/except
try:
x = int(input("Enter a number: "))
result = 100 / x
print(f"100 / {x} = {result}")
except ValueError:
print("That is not a valid number")
except ZeroDivisionError:
print("Cannot divide by zero")
# Catch multiple exceptions in one clause
try:
data = int("not a number")
except (ValueError, TypeError) as e:
print(f"Conversion error: {e}")
# Access exception with 'as e'
try:
with open("missing.txt") as f:
content = f.read()
except FileNotFoundError as e:
print(f"File not found: {e.filename}")
print(f"Error code: {e.errno}")
content = ""
# Never use bare except - it swallows everything including Ctrl+C
# BAD:
# try:
# ...
# except: # catches KeyboardInterrupt, SystemExit too
# pass
# GOOD: catch specific or at least Exception
try:
risky_operation = 1 / 0
except Exception as e:
print(f"Caught: {type(e).__name__}: {e}")
# Check exception type
try:
result = 1 / 0
except Exception as e:
if isinstance(e, ZeroDivisionError):
print("Zero division!")
raise # re-raise the same exception
else and finally
# else: runs only if NO exception was raised
try:
result = int("42")
except ValueError:
print("Bad input")
else:
print(f"Success: {result}") # runs because no exception
# finally: ALWAYS runs (cleanup code)
import os
f = None
try:
f = open("data.txt", "w")
f.write("test")
raise RuntimeError("something failed")
except RuntimeError as e:
print(f"Error: {e}")
finally:
if f:
f.close()
print("File closed") # runs even though exception occurred
# With 'with' statement this is cleaner:
try:
with open("data.txt", "w") as f:
f.write("test")
raise RuntimeError("something failed")
except RuntimeError as e:
print(f"Error: {e}")
# file is automatically closed by 'with'
# Full try/except/else/finally structure
def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("Division by zero!")
return None
else:
print(f"Result: {result}") # only if no exception
return result
finally:
print("divide() called") # always runs
print(divide(10, 2)) # "Result: 5.0", "divide() called", 5.0
print(divide(10, 0)) # "Division by zero!", "divide() called", None
# finally and return: finally ALWAYS runs
def risky():
try:
return "try"
finally:
return "finally" # overrides "try" return!
print(risky()) # "finally" (finally return takes precedence)
raise and re-raise
# Raise a built-in exception
def get_age(age):
if not isinstance(age, int):
raise TypeError(f"age must be int, got {type(age).__name__}")
if age < 0:
raise ValueError(f"age cannot be negative: {age}")
if age > 150:
raise ValueError(f"age seems unrealistic: {age}")
return age
try:
get_age(-5)
except ValueError as e:
print(e) # age cannot be negative: -5
# Re-raise: run code, then propagate the original exception
def process_file(path):
try:
with open(path) as f:
return f.read()
except OSError:
print(f"Failed to read {path}")
raise # re-raises the same exception with original traceback
# Raise from inside except (avoid losing original exception)
def connect(url):
try:
# simulate connection
raise ConnectionRefusedError("port 5432")
except ConnectionRefusedError as e:
raise RuntimeError(f"Database unavailable: {url}") from e
# raise ... from None: suppress chained traceback
def parse_config(text):
try:
import json
return json.loads(text)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid config format: {e}") from None
# caller doesn't need to see the JSONDecodeError details
Custom Exceptions
# Simple custom exception
class AppError(Exception):
"""Base class for all application errors."""
pass
class ValidationError(AppError):
"""Raised when input validation fails."""
pass
class NotFoundError(AppError):
"""Raised when a resource is not found."""
pass
# Custom exception with extra attributes
class HttpError(Exception):
def __init__(self, status_code, message):
super().__init__(message) # sets args[0] and str(self)
self.status_code = status_code
self.message = message
def __str__(self):
return f"HTTP {self.status_code}: {self.message}"
# Usage
try:
raise HttpError(404, "Not Found")
except HttpError as e:
print(e.status_code) # 404
print(e.message) # Not Found
print(str(e)) # HTTP 404: Not Found
# Exception hierarchy for a library
class DatabaseError(Exception):
"""Base class for database errors."""
pass
class ConnectionError(DatabaseError):
def __init__(self, host, port, original=None):
super().__init__(f"Cannot connect to {host}:{port}")
self.host = host
self.port = port
self.original = original
class QueryError(DatabaseError):
def __init__(self, query, message):
super().__init__(f"Query failed: {message}")
self.query = query
# Callers can catch at any level
try:
raise ConnectionError("localhost", 5432)
except ConnectionError:
print("Connection issue - retry")
except DatabaseError:
print("Some database error")
except Exception:
print("Unexpected error")
Exception Chaining
# Implicit chaining - Python auto-chains when raising inside except
try:
try:
int("not a number")
except ValueError:
open("missing.txt") # raises FileNotFoundError
except FileNotFoundError as e:
print(e.__context__) # ValueError: invalid literal...
# Traceback shows both exceptions
# Explicit chaining with 'from'
class ServiceError(Exception):
pass
def call_api():
try:
# simulate failed HTTP call
raise ConnectionRefusedError("Port 443")
except ConnectionRefusedError as e:
raise ServiceError("API unavailable") from e
try:
call_api()
except ServiceError as e:
print(e) # API unavailable
print(e.__cause__) # ConnectionRefusedError - the original
# Traceback: "The above exception was the direct cause of..."
# Suppress chaining with 'from None'
def load_config(path):
try:
import json
with open(path) as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
raise ValueError(f"Cannot load config from {path}") from None
# Original exception details hidden from caller
# Context manager for error suppression
from contextlib import suppress
with suppress(FileNotFoundError):
import os
os.remove("temp_file.txt") # silently ignore if missing
Exception Hierarchy
# Common built-in exceptions and when they occur
# ValueError - right type, wrong value
int("abc") # ValueError: invalid literal
int("") # ValueError
[1,2,3].index(99) # ValueError: 99 is not in list
# TypeError - wrong type
"2" + 2 # TypeError: can only concatenate str (not "int")
len(42) # TypeError: object of type 'int' has no len()
# KeyError - dict key not found
d = {"a": 1}
d["b"] # KeyError: 'b'
# IndexError - sequence index out of range
lst = [1, 2, 3]
lst[5] # IndexError: list index out of range
# AttributeError - object has no attribute
None.strip() # AttributeError: 'NoneType' object has no attribute 'strip'
# NameError - name not defined
print(undefined_var) # NameError: name 'undefined_var' is not defined
# ImportError / ModuleNotFoundError
import nonexistent # ModuleNotFoundError: No module named 'nonexistent'
# FileNotFoundError (subclass of OSError)
open("missing.txt") # FileNotFoundError
# OverflowError, ZeroDivisionError, RecursionError
1 / 0 # ZeroDivisionError
# StopIteration - iterator exhausted
it = iter([1])
next(it)
next(it) # StopIteration
# Best practice: catch the most specific exception possible
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
return float("inf")
Catch only the exceptions you know how to handle. A broad except Exception hides bugs. If you genuinely want to catch everything (for logging), still re-raise: except Exception: log_error(); raise. Never silently swallow exceptions with except Exception: pass unless you are absolutely certain silence is correct.