Python Basics — try/except/else/finally
try/except/else/finally
Python’s error-handling system centers around four keywords: try, except, else, and finally. Together, they give you complete control over how your program responds to errors.
Basic try/except
try:
number = int(input("Enter a number: "))
print(f"You entered: {number}")
except ValueError:
print("That's not a valid number!")
How it works:
- Python runs the code in the
tryblock - If no error occurs, the
exceptblock is skipped - If a
ValueErroroccurs, Python jumps to theexceptblock - Program continues running after the except block
Catching Specific Exceptions
Always catch specific exceptions rather than all exceptions:
# ✅ Specific — catches only what you expect
try:
value = int("abc")
except ValueError:
print("Not a valid integer")
# ❌ Too broad — hides bugs
try:
value = int("abc")
except:
print("Something went wrong")
Multiple except Blocks
Handle different errors differently:
def safe_divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("Error: Cannot divide by zero")
return None
except TypeError:
print("Error: Both arguments must be numbers")
return None
else:
return result
print(safe_divide(10, 3)) # 3.3333...
print(safe_divide(10, 0)) # Error: Cannot divide by zero → None
print(safe_divide("10", 3)) # Error: Both arguments must be numbers → None
Catching Multiple Exceptions in One Block
try:
# Some risky operation
data = {"key": "value"}
result = data["missing"]
except (KeyError, IndexError) as e:
print(f"Lookup error: {e}")
Accessing the Exception Object
Use as to capture the exception details:
try:
number = int("not_a_number")
except ValueError as e:
print(f"Error type: {type(e).__name__}")
print(f"Error message: {e}")
# Error type: ValueError
# Error message: invalid literal for int() with base 10: 'not_a_number'
The else Clause
The else block runs only when no exception occurred. Use it to separate the “risky” code from the “success” code:
try:
number = int(input("Enter a number: "))
except ValueError:
print("Invalid input!")
else:
# Only runs if try succeeded
print(f"Your number squared: {number ** 2}")
Why Use else?
# ❌ Without else — error in process_data() is wrongly caught
try:
data = json.load(open("data.json"))
result = process_data(data) # Bug here gets caught too!
except ValueError:
print("Invalid JSON")
# ✅ With else — only JSON parsing errors are caught
import json
try:
data = json.load(open("data.json"))
except ValueError:
print("Invalid JSON")
else:
result = process_data(data) # Errors here propagate normally
The finally Clause
The finally block always runs, whether an exception occurred or not:
try:
f = open("data.txt", "r")
content = f.read()
except FileNotFoundError:
print("File not found!")
finally:
print("Cleanup complete") # Always runs
Common Use Cases for finally
# Resource cleanup
connection = None
try:
connection = create_database_connection()
connection.execute("SELECT * FROM users")
except DatabaseError as e:
print(f"Database error: {e}")
finally:
if connection:
connection.close()
print("Connection closed")
# Timer / performance measurement
import time
start = time.time()
try:
result = expensive_operation()
except Exception as e:
print(f"Failed: {e}")
finally:
elapsed = time.time() - start
print(f"Operation took {elapsed:.2f} seconds")
The Complete Pattern
try:
# Code that might raise an exception
result = risky_operation()
except SpecificError as e:
# Handle the specific error
log_error(e)
except AnotherError as e:
# Handle a different error
handle_another(e)
else:
# Runs ONLY if no exception occurred
process_result(result)
finally:
# ALWAYS runs — cleanup code goes here
cleanup()
Full Example: File Processing
import json
def load_config(filename):
"""Load and validate a JSON config file."""
try:
with open(filename, "r") as f:
config = json.load(f)
except FileNotFoundError:
print(f"Config file '{filename}' not found, using defaults")
return {"debug": False, "port": 8080}
except json.JSONDecodeError as e:
print(f"Invalid JSON in '{filename}': {e}")
return {"debug": False, "port": 8080}
else:
print(f"Config loaded from '{filename}'")
return config
finally:
print("Config initialization complete")
config = load_config("app_config.json")
print(f"Debug mode: {config.get('debug', False)}")
Bare except (And Why to Avoid It)
# ❌ Bare except — catches EVERYTHING, including KeyboardInterrupt
try:
data = fetch_data()
except:
print("Error!")
# ❌ Catching Exception is slightly better but still too broad
try:
data = fetch_data()
except Exception:
print("Error!")
# ✅ Catch specific exceptions
try:
data = fetch_data()
except ConnectionError:
print("Could not connect to server")
except TimeoutError:
print("Request timed out")
Why bare except is dangerous:
- Hides bugs you didn’t anticipate
- Catches
KeyboardInterrupt(Ctrl+C) — user can’t stop the program - Catches
SystemExit— program ignores exit requests - Makes debugging nearly impossible
Nested try/except
def safe_read_json(filename):
"""Read JSON with multiple layers of error handling."""
try:
with open(filename, "r") as f:
try:
data = json.load(f)
except json.JSONDecodeError:
print("File exists but contains invalid JSON")
return None
except FileNotFoundError:
print(f"File '{filename}' not found")
return None
return data
Exception Handling Flow
try block
│
├── No exception → else block → finally block → continue
│
└── Exception raised
│
├── Matching except found → except block → finally block → continue
│
└── No matching except → finally block → exception propagates ↑
Summary
| Keyword | Purpose | When It Runs |
|---|---|---|
try |
Code that might raise an exception | Always attempted |
except |
Handle specific exceptions | Only on matching exception |
else |
Success code (no exception occurred) | Only when try succeeds |
finally |
Cleanup code | Always — error or not |
- Catch specific exceptions — never use bare
except - Use
elseto separate risky code from success code - Use
finallyfor cleanup (closing files, connections, etc.) - Access exception details with
except ErrorType as e
Next up: Raising Exceptions →