Python Basics — try/except/else/finally

4 minute read

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:

  1. Python runs the code in the try block
  2. If no error occurs, the except block is skipped
  3. If a ValueError occurs, Python jumps to the except block
  4. 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 else to separate risky code from success code
  • Use finally for cleanup (closing files, connections, etc.)
  • Access exception details with except ErrorType as e

Next up: Raising Exceptions →

Updated: