Exception Handling: else and finally Blocks
The complete Python exception-handling structure uses four clauses: try for risky code, except for error handlers, else for success-only code, and finally for guaranteed cleanup. While try-except handles errors, adding else and finally gives you fine-grained control over execution flow and resource management. This guide shows when and how to use each clause.
Key Takeaways
elseblock executes only if thetryblock completes without raising any exceptionfinallyblock always executes, regardless of success/failure/uncaught exceptions—essential for cleanupfinallyruns even if an exception is not caught, making it ideal for closing files and releasing resources- The full structure (
try-except-else-finally) clarifies intent: risky code, error handling, success logic, and cleanup
Understanding the else Block: Execute on Success Only
The else block runs only when the try block succeeds—no exception was raised. This separates "at-risk" code from "success-dependent" code, making logic clearer and reducing the try block's scope.
Syntax and Basic Example
try:
# Code that might raise an exception
risky_operation()
except SomeException:
# Handle the error
print("Error occurred")
else:
# Code that runs ONLY if try succeeded
print("Operation succeeded!")
Practical Example: Division with Input Validation
# Without else: success logic mixed with error handling
try:
numerator = int(input("Numerator: "))
denominator = int(input("Denominator: "))
result = numerator / denominator
print(f"Result: {result:.2f}") # Success message inside try
except ValueError:
print("Please enter numbers only.")
except ZeroDivisionError:
print("Cannot divide by zero.")
# With else: clear separation
try:
numerator = int(input("Numerator: "))
denominator = int(input("Denominator: "))
result = numerator / denominator
except ValueError:
print("Please enter numbers only.")
except ZeroDivisionError:
print("Cannot divide by zero.")
else:
# Runs only if both inputs are valid numbers AND denominator is not zero
print(f"Success! Result: {result:.2f}")
The else version clearly communicates: the try block is for parsing and calculation (dangerous), and the else block is the "happy path" outcome.
Multiple except Blocks with else
def parse_config(filename):
"""Reads and parses a config file."""
try:
with open(filename, 'r') as f:
lines = f.readlines()
config = {}
for line in lines:
key, value = line.strip().split('=')
config[key] = value
except FileNotFoundError:
print(f"Config file '{filename}' not found.")
except ValueError:
print("Invalid config format (expected 'key=value').")
else:
# Only reached if file was opened and parsed successfully
print(f"Loaded {len(config)} configuration items.")
return config
return None
parse_config("app.conf")
Understanding the finally Block: Guaranteed Cleanup
The finally block is guaranteed to run, no matter what. It executes whether:
- The
tryblock succeeds. - An
exceptblock catches an exception. - An exception occurs but is not caught.
- A
returnstatement exits the function early.
This makes finally perfect for cleanup: closing files, releasing connections, freeing memory.
Syntax and Basic Example
try:
# Risky code
resource = open_resource()
except Exception as e:
# Handle error
print(f"Error: {e}")
finally:
# Always runs—cleanup code
close_resource(resource)
Real-World Example: File Operations
# Without finally: file might not close if exception occurs
try:
f = open('data.txt', 'r')
content = f.read()
process(content)
f.close() # Skipped if exception occurs before this line
except IOError as e:
print(f"Error: {e}")
# With finally: file is guaranteed to close
f = None
try:
f = open('data.txt', 'r')
content = f.read()
process(content)
except IOError as e:
print(f"Error: {e}")
finally:
if f:
f.close() # Always runs
print("File closed.")
In modern Python, the with statement handles this automatically, but finally shows the concept.
finally Runs Even With Unhandled Exceptions
def risky_function():
try:
print("Inside try block")
raise ValueError("Custom error")
finally:
print("Inside finally—always runs even if exception is not caught")
try:
risky_function()
except ValueError as e:
print(f"Caught: {e}")
# Output:
# Inside try block
# Inside finally—always runs even if exception is not caught
# Caught: Custom error
finally Runs Even With Early return
def authenticate(username, password):
"""Returns True if credentials are valid."""
print("Attempting authentication...")
try:
if username == "admin" and password == "secret":
return True # Early exit
raise ValueError("Invalid credentials")
except ValueError:
print("Authentication failed.")
return False
finally:
print("Cleaning up authentication resources.")
result = authenticate("admin", "secret")
print(f"Result: {result}")
# Output:
# Attempting authentication...
# Cleaning up authentication resources.
# Result: True
The Complete try-except-else-finally Structure
Combining all four clauses creates a robust, self-documenting error-handling flow:
try:
# Code that might raise an exception
risky_code()
except SpecificException:
# Handle this specific error
handle_error()
except AnotherException:
# Handle another error
handle_other_error()
else:
# Runs ONLY if try succeeds (no exception)
handle_success()
finally:
# ALWAYS runs—cleanup
cleanup()
Practical Example: Robust File Processing
print("Starting data processing...")
input_file = None
output_file = None
try:
# Try to open, read, and process data
input_file = open('input.txt', 'r')
data = input_file.read()
# Parse and process
numbers = [int(x) for x in data.split(',')]
average = sum(numbers) / len(numbers)
except FileNotFoundError:
print("Error: 'input.txt' not found.")
except ValueError:
print("Error: Data contains non-numeric values.")
except ZeroDivisionError:
print("Error: No numbers to process.")
else:
# Only runs if parsing succeeded
print(f"Average calculated: {average:.2f}")
# Write result to output file
try:
output_file = open('output.txt', 'w')
output_file.write(f"Average: {average:.2f}")
print("Result written to 'output.txt'.")
except IOError as e:
print(f"Error writing output: {e}")
finally:
# Always runs—close any open files
if input_file:
input_file.close()
print("Closed input file.")
if output_file:
output_file.close()
print("Closed output file.")
print("--- Processing complete. ---")
Database Connection Example
def update_user(user_id, name):
"""Updates user in database with guaranteed connection cleanup."""
connection = None
try:
connection = db.connect() # Opens connection
connection.execute("UPDATE users SET name = ? WHERE id = ?", (name, user_id))
connection.commit()
except db.ConnectionError:
print("Database connection failed.")
except db.ExecutionError:
print("Query execution failed.")
else:
print(f"User {user_id} updated successfully.")
finally:
if connection:
connection.close() # Always closes, even if exception occurred
print("Connection closed.")
Advanced Patterns: Nested try-except-else-finally
Sometimes you need multiple levels of error handling:
def process_multiple_files(filenames):
"""Processes multiple files with nested error handling."""
results = []
for filename in filenames:
try:
f = None
try:
f = open(filename, 'r')
content = f.read()
except FileNotFoundError:
print(f"Skipping {filename}: not found.")
else:
# Only processes if file was opened
results.append((filename, len(content)))
finally:
if f:
f.close()
except Exception as e:
print(f"Unexpected error with {filename}: {e}")
return results
files = ['a.txt', 'b.txt', 'missing.txt']
process_multiple_files(files)
Comparison: finally vs. else
| Clause | When It Runs | Use Case |
|---|---|---|
else | Only if try succeeds | Success-dependent logic (e.g., process data, write output) |
finally | Always | Cleanup (close files, release connections, free resources) |
try:
data = load_file("config.json")
except FileNotFoundError:
print("Config not found.")
except json.JSONDecodeError:
print("Invalid JSON.")
else:
# Runs if load succeeds
apply_config(data)
finally:
# Always runs
print("Application initialized.")
Best Practices for Exception Handling
1. Keep try Blocks Focused
# Bad: too much code in try block
try:
data = load_data()
validate_schema(data)
transform_data(data)
save_data(data)
generate_report(data) # Unrelated logic
except Exception as e:
print(f"Error: {e}")
# Good: focused try block, success logic in else
try:
data = load_data()
validate_schema(data)
transform_data(data)
except FileNotFoundError:
print("Data file not found.")
except ValueError:
print("Invalid data format.")
else:
save_data(data)
generate_report(data)
2. Always Clean Up in finally
# Always use finally or 'with' for resource cleanup
lock = None
try:
lock = acquire_lock()
critical_section()
except LockError as e:
print(f"Lock acquisition failed: {e}")
finally:
if lock:
release_lock(lock)
3. Use Context Managers (with statement) for Files
# Modern best practice: 'with' statement handles cleanup automatically
with open('file.txt', 'r') as f:
data = f.read()
process(data)
# File is automatically closed, no finally needed
Frequently Asked Questions
What's the difference between except and else?
except runs if an exception occurs in the try block. else runs if no exception occurs. They are opposite conditions.
try:
result = 10 / 2
except ZeroDivisionError:
print("Caught error") # Does NOT run
else:
print(f"Success: {result}") # Runs (result = 5.0)
Does finally run if I use return in try?
Yes. finally runs even if try, except, or else has a return statement.
def test():
try:
return "From try"
finally:
print("Finally runs even with return")
result = test()
# Output:
# Finally runs even with return
# result = "From try"
Can I have finally without except?
Yes. You can have try-finally (without except) and try-else-finally (without except), but not valid exception handling.
try:
data = load_file()
finally:
cleanup() # Runs even if load_file raises an exception
Should I use finally or the with statement for file handling?
Use with (context managers)—it's cleaner and more Pythonic. Use finally when you need explicit control or when dealing with multiple resources.
# Best: with statement
with open('file.txt') as f:
process(f)
# Good: try-finally (if with isn't available)
f = open('file.txt')
try:
process(f)
finally:
f.close()
What if an exception occurs in finally?
If finally raises an exception, it replaces any exception from try or except. To avoid this, use try-except inside finally:
try:
risky()
finally:
try:
cleanup()
except Exception as e:
print(f"Cleanup failed: {e}")