Cleanup with finally¶
In this tutorial, you will learn how to use the finally and else clauses in try/except blocks, and how to use context managers with the with statement for reliable resource cleanup.
Time commitment: 15–20 minutes
Prerequisites:
- Completion of Raising exceptions
- Understanding of
try/exceptblocks, exception types, and theraisestatement
Learning objectives¶
By the end of this tutorial, you will be able to:
- Use the
finallyclause to guarantee cleanup code runs - Use the
elseclause to run code only when no exception occurs - Write the full
try/except/else/finallystructure - Use context managers with the
withstatement for resource management
The problem: unreliable cleanup¶
When your code opens a file, establishes a network connection, or acquires any other resource, you need to release that resource when you are finished. But what if an exception occurs before the cleanup code runs?
Consider this example.
import tempfile
import os
# Create a temporary file for demonstration
temp_path = os.path.join(tempfile.gettempdir(), "demo_data.txt")
with open(temp_path, "w", encoding="utf-8") as f:
f.write("10\n20\nabc\n40\n")
print(f"Created temporary file: {temp_path}")
# Without finally, an exception could leave the file open
f = open(temp_path, "r", encoding="utf-8")
try:
content = f.read()
print(f"Read {len(content)} characters")
except FileNotFoundError:
print("File not found")
# If an unexpected exception occurs above, the file stays open
f.close()
print("File closed successfully")
The code above has a flaw: if an unexpected exception occurs between open() and f.close(), the file will never be closed. The finally clause solves this problem.
The finally clause¶
The finally clause contains code that always runs, whether an exception occurred or not. This makes it ideal for cleanup operations.
try:
# Code that might raise an exception
except SomeException:
# Handle the exception
finally:
# This always runs, no matter what
# With finally, the file is always closed
f = open(temp_path, "r", encoding="utf-8")
try:
content = f.read()
print(f"Read {len(content)} characters")
except FileNotFoundError:
print("File not found")
finally:
f.close()
print("File closed (guaranteed by finally)")
The finally block runs regardless of whether an exception was raised, whether it was handled, or whether the code completed normally.
finally runs even when exceptions are unhandled¶
An important property of finally is that it runs even when the exception is not handled by any except clause. Let us demonstrate this with a safe example.
def demonstrate_finally(value: int) -> str:
"""Demonstrate that finally always runs."""
resource = "acquired"
try:
if value == 0:
raise ValueError("Zero is not allowed")
return f"Success: {100 / value}"
except ValueError as e:
return f"Handled: {e}"
finally:
resource = "released"
print(f"Finally block executed. Resource: {resource}")
# Normal execution: finally still runs
print(demonstrate_finally(5))
print()
# Handled exception: finally still runs
print(demonstrate_finally(0))
Notice that the finally block executed in both cases, even when a return statement was reached in the try or except block.
The else clause¶
The else clause runs only when the try block completes without raising an exception. This is useful for code that should run only on success.
try:
# Code that might raise an exception
except SomeException:
# Handle the exception
else:
# Runs only if no exception occurred
def safe_divide(a: float, b: float) -> float | None:
"""Divide two numbers, demonstrating the else clause."""
try:
result = a / b
except ZeroDivisionError:
print("Division by zero is not allowed.")
return None
else:
print(f"Division successful: {a} / {b} = {result}")
return result
# Success: else clause runs
safe_divide(10, 3)
print()
# Failure: else clause does not run
safe_divide(10, 0)
Using else is better than putting the success code inside try, because it prevents accidentally handling exceptions raised by the success code itself.
The full structure: try/except/else/finally¶
You can combine all four clauses for complete control over exception handling.
try:
# Attempt the operation
except SomeException:
# Handle the exception
else:
# Runs only if no exception occurred
finally:
# Always runs (cleanup)
The clauses must appear in this order: try, then except, then else, then finally.
def read_numbers_from_file(filepath: str) -> list[int]:
"""Read integers from a file, one per line.
Args:
filepath: Path to the file containing numbers.
Returns:
A list of integers read from the file.
"""
numbers = []
f = None
try:
f = open(filepath, "r", encoding="utf-8")
for line in f:
numbers.append(int(line.strip()))
except FileNotFoundError:
print(f"File not found: {filepath}")
except ValueError as e:
print(f"Invalid data in file: {e}")
else:
print(f"Successfully read {len(numbers)} numbers")
finally:
if f is not None:
f.close()
print("File closed")
return numbers
# Create a valid file
valid_path = os.path.join(tempfile.gettempdir(), "valid_numbers.txt")
with open(valid_path, "w", encoding="utf-8") as f:
f.write("10\n20\n30\n")
print("--- Reading valid file ---")
result = read_numbers_from_file(valid_path)
print(f"Numbers: {result}")
print()
print("--- Reading nonexistent file ---")
result = read_numbers_from_file("/nonexistent/path.txt")
print(f"Numbers: {result}")
print()
print("--- Reading file with invalid data ---")
result = read_numbers_from_file(temp_path)
print(f"Numbers: {result}")
Context managers: the with statement¶
Writing try/finally blocks for every resource can be tedious and error-prone. Python provides a cleaner alternative: the context manager using the with statement.
A context manager automatically handles setup and cleanup. The most common example is opening files.
# Without context manager (verbose and error-prone)
f = open(valid_path, "r", encoding="utf-8")
try:
content = f.read()
finally:
f.close()
print(f"Without context manager: read {len(content)} characters")
# With context manager (clean and safe)
with open(valid_path, "r", encoding="utf-8") as f:
content = f.read()
print(f"With context manager: read {len(content)} characters")
Both examples do the same thing, but the with statement is shorter, clearer, and guarantees the file will be closed even if an exception occurs.
When the with block ends (whether normally or because of an exception), Python automatically calls the cleanup method on the context manager.
Combining with and try/except¶
You can combine context managers with try/except blocks for both resource management and exception handling.
def count_lines(filepath: str) -> int:
"""Count the number of lines in a file.
Args:
filepath: Path to the file.
Returns:
The number of lines, or -1 if the file cannot be read.
"""
try:
with open(filepath, "r", encoding="utf-8") as f:
return sum(1 for _ in f)
except FileNotFoundError:
print(f"File not found: {filepath}")
return -1
except PermissionError:
print(f"Permission denied: {filepath}")
return -1
print(f"Lines in valid file: {count_lines(valid_path)}")
print(f"Lines in missing file: {count_lines('/nonexistent/file.txt')}")
The with statement handles closing the file, whilst the try/except block handles the exceptions. This is the recommended pattern for working with files and other resources.
def read_and_sum(filepath: str) -> float:
"""Read numbers from a file and return their sum."""
pass # Replace this with your implementation
Click to reveal the solution
def read_and_sum(filepath: str) -> float:
"""Read numbers from a file and return their sum."""
try:
with open(filepath, "r", encoding="utf-8") as f:
total = sum(float(line.strip()) for line in f)
except FileNotFoundError:
print(f"File not found: {filepath}")
return 0
except ValueError as e:
print(f"Invalid data: {e}")
return 0
else:
return total
Exercise 2: Try/except/else/finally¶
Write a function called safe_reciprocal that takes a number and returns its reciprocal (1 divided by the number). Use the full try/except/else/finally structure:
try: Perform the divisionexcept: HandleZeroDivisionErrorand returnNoneelse: Print the successful resultfinally: Print"Calculation complete"
def safe_reciprocal(number: float) -> float | None:
"""Return the reciprocal of a number, or None if the number is zero."""
pass # Replace this with your implementation
Click to reveal the solution
def safe_reciprocal(number: float) -> float | None:
"""Return the reciprocal of a number, or None if the number is zero."""
try:
result = 1 / number
except ZeroDivisionError:
print("Cannot compute the reciprocal of zero.")
return None
else:
print(f"Reciprocal of {number} is {result}")
return result
finally:
print("Calculation complete")
Cleanup¶
Let us remove the temporary files we created during this tutorial.
# Remove temporary files
for path in [temp_path, valid_path]:
try:
os.remove(path)
print(f"Removed: {path}")
except FileNotFoundError:
pass
Summary¶
In this tutorial, you learned the following:
- The
finallyclause always runs, making it ideal for cleanup code - The
elseclause runs only when no exception occurred in thetryblock - The full structure is
try/except/else/finally, and the clauses must appear in this order - Context managers (the
withstatement) provide a cleaner alternative totry/finallyfor resource management - Combine context managers with
try/exceptfor both resource management and exception handling
Congratulations! You have completed the tutorials section. You now have a solid foundation in exception handling with Python. To continue learning, explore the Recipes for practical solutions to specific problems, or dive into the Reference for detailed technical documentation.