Python Try Except Finally

When something goes wrong while a Python program is running — a file is missing, a user types text instead of a number, a network call times out — Python raises an exception. Without any handling, that exception crashes the program and prints a traceback. The try, except, else, and finally statement is how you intercept that failure, decide what to do about it, and make sure cleanup code still runs no matter what happens. Mastering it is what separates a script that dies on the first bad input from a program that handles the real world gracefully.

Overview: How Exception Handling Works

Every error in Python is represented by an exception object — an instance of a class like ValueError, TypeError, or ZeroDivisionError, all of which ultimately inherit from the built-in BaseException (most of them from its subclass Exception). When code raises an exception, Python does not simply stop: it immediately abandons the rest of the current statement block and starts unwinding the call stack, looking outward through each enclosing try block for a matching except clause. If it finds one whose exception type matches (or is a superclass of) the raised exception, execution jumps into that handler. If no handler anywhere in the call stack matches, the exception propagates all the way to the top level, Python prints the traceback, and the program terminates.

A try statement can have four kinds of clauses, and understanding when each one runs is the key to using them correctly:

  • try — the code you want to attempt. Python starts executing it and watches for exceptions.
  • except — runs only if an exception matching its type was raised inside the try block. You can have several of these for different exception types.
  • else — runs only if the try block completed with no exception at all. It’s optional, and it lets you separate "code that might fail" from "code that should only run on success."
  • finally — runs unconditionally: whether the try succeeded, an exception was caught, an exception was not caught, or the block exited via return, break, or continue. It is Python’s guarantee for cleanup code (closing files, releasing locks, disconnecting from a database).

Internally, once an exception object is created, Python attaches a __traceback__ to it recording every frame the error passed through, and (since Python 3) an exception raised while handling another one gets its __context__ set automatically, which is why you sometimes see "During handling of the above exception, another exception occurred" in a traceback.

Common built-in exception types

Exception Raised when…
ValueError A value has the right type but an inappropriate value (e.g. int("abc"))
TypeError An operation is applied to an object of the wrong type
ZeroDivisionError Dividing or taking the modulus by zero
KeyError A dictionary key does not exist
IndexError A sequence index is out of range
FileNotFoundError Opening a file path that doesn’t exist
AttributeError An object has no attribute with that name

Syntax

The full general form looks like this:

try:
    pass  # code that might raise an exception
except SomeException:
    pass  # runs if SomeException is raised
except (AnotherException, YetAnotherException) as e:
    pass  # runs if either is raised; e holds the exception object
else:
    pass  # runs only if no exception occurred
finally:
    pass  # always runs, exception or not
  • except SomeException: catches only that exact class (or its subclasses).
  • except (A, B) as e: catches several types in one clause; e is bound to the raised exception instance, so you can inspect str(e), e.args, or custom attributes.
  • You only need the clauses you actually use — except, else, and finally are each optional, but a bare try must have at least one except or a finally.
  • Clauses are checked top to bottom; put more specific exception types before more general ones, since Python uses the first match.

Examples

Example 1: try / except / else / finally together

def safe_divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("Error: Cannot divide by zero.")
        return None
    else:
        print("Division succeeded.")
        return result
    finally:
        print("Division attempt finished.")

print(safe_divide(10, 2))
print(safe_divide(10, 0))

Output:

Division succeeded.
Division attempt finished.
5.0
Error: Cannot divide by zero.
Division attempt finished.
None

For safe_divide(10, 2), the division succeeds, so the else block runs and prints "Division succeeded." before its return result executes. Crucially, finally still runs before the function actually hands control back to the caller, which is why "Division attempt finished." appears before 5.0 is printed by the outer print(). For safe_divide(10, 0), the ZeroDivisionError is caught, the else block is skipped entirely (it only runs on success), and finally still runs before returning None.

Example 2: multiple except clauses in a loop

def parse_config_value(raw_value):
    try:
        value = int(raw_value)
        threshold = 100 / value
    except ValueError:
        print(f"'{raw_value}' is not a valid integer.")
    except ZeroDivisionError:
        print("Value cannot be zero.")
    else:
        print(f"Threshold computed: {threshold}")
    finally:
        print("Finished processing one config value.\n")

for raw in ["20", "0", "abc"]:
    parse_config_value(raw)

Output:

Threshold computed: 5.0
Finished processing one config value.

Value cannot be zero.
Finished processing one config value.

'abc' is not a valid integer.
Finished processing one config value.

Each iteration hits a different clause: "20" converts fine and only fails when dividing (no — it doesn’t fail at all, 100/20 works, so else runs); "0" converts fine but then 100 / 0 raises ZeroDivisionError; "abc" fails at int("abc") with ValueError. Notice that finally fires every single time regardless of which branch was taken — that’s what makes it reliable for logging or cleanup that must always happen.

Example 3: custom exceptions with else and finally

class InsufficientFundsError(Exception):
    """Raised when a withdrawal exceeds the available balance."""
    def __init__(self, balance, amount):
        super().__init__(f"Cannot withdraw {amount}; balance is only {balance}.")
        self.balance = balance
        self.amount = amount

def withdraw(balance, amount):
    try:
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive.")
        if amount > balance:
            raise InsufficientFundsError(balance, amount)
        balance -= amount
    except InsufficientFundsError as e:
        print(f"Transaction blocked: {e}")
    except ValueError as e:
        print(f"Invalid input: {e}")
    else:
        print(f"Withdrew {amount}. New balance: {balance}")
    finally:
        print("Transaction attempt logged.")
    return balance

balance = 100
balance = withdraw(balance, 30)
balance = withdraw(balance, 200)
balance = withdraw(balance, -5)
print(f"Final balance: {balance}")

Output:

Withdrew 30. New balance: 70
Transaction attempt logged.
Transaction blocked: Cannot withdraw 200; balance is only 70.
Transaction attempt logged.
Invalid input: Withdrawal amount must be positive.
Transaction attempt logged.
Final balance: 70

This example defines a custom exception, InsufficientFundsError, by subclassing Exception and building a descriptive message in __init__. Notice that when an exception is raised inside try, the assignment balance -= amount never runs, so the balance is left unchanged for the failed attempts — only the successful withdrawal of 30 actually reduces it. Custom exceptions like this are extremely common in real applications because they let calling code distinguish "this failed because of business rules" from "this failed because of a bug," using except InsufficientFundsError instead of a generic catch.

Under the Hood: What Python Actually Does

When Python enters a try block, it doesn’t do anything special upfront — there’s no cost to a try that never raises. It’s only when a raise statement executes (either explicitly, or implicitly inside a built-in operation like division or indexing) that Python creates the exception object, attaches the current call stack to it as a traceback, and begins searching outward for a handler. This search happens frame by frame: the current function, then whoever called it, and so on, checking each enclosing try‘s except clauses in order for a type match using isinstance-style checks against the exception class hierarchy.

The finally block is special because Python guarantees it executes no matter how control leaves the try/except/else structure — normal completion, an exception, or even a return, break, or continue statement inside the block. If the try block hits a return, Python computes the return value, then runs finally before actually handing control back to the caller — which is why a return or raise inside finally can silently override whatever the try or except was trying to do (see Common Mistakes below). If no handler catches the exception anywhere up the stack, it eventually reaches the top level, Python prints the traceback to stderr, and the interpreter exits with a non-zero status.

You can also deliberately preserve the connection between two errors using exception chaining: raise NewError("...") from original_error sets __cause__ on the new exception, so the traceback shows both errors and explains that the second was raised while handling the first — useful when you catch a low-level exception and re-raise a more meaningful, higher-level one.

Common Mistakes

Mistake 1: Catching everything with a bare except:

Writing except: with no exception type catches literally everything, including KeyboardInterrupt (Ctrl+C) and SystemExit, which means your program can become impossible to stop cleanly and real bugs get silently swallowed. The fix is to always name the exception type you expect, such as except ValueError:, or at worst except Exception:, which excludes those system-level signals while still catching ordinary errors.

Mistake 2: Putting return inside finally

A return (or unconditional raise) inside a finally block silently discards whatever the try or except block was about to return or raise. Here is the buggy version:

def risky_calculation(x):
    try:
        return 10 / x
    except ZeroDivisionError:
        print("Caught division by zero")
        raise
    finally:
        return -1

print(risky_calculation(0))
print(risky_calculation(2))

Output:

Caught division by zero
-1
-1

Even though except re-raises the ZeroDivisionError with a bare raise, the finally block’s return -1 overrides it completely — the exception never reaches the caller, and even the successful case (risky_calculation(2), which should return 5.0) gets clobbered into -1 too. The corrected version keeps finally for cleanup only, with no return or raise in it:

def risky_calculation(x):
    try:
        return 10 / x
    except ZeroDivisionError:
        print("Caught division by zero")
        raise
    finally:
        print("Cleanup complete")

print(risky_calculation(2))
try:
    risky_calculation(0)
except ZeroDivisionError:
    print("Exception propagated correctly")

Output:

Cleanup complete
5.0
Caught division by zero
Cleanup complete
Exception propagated correctly

Best Practices

  • Catch the most specific exception type you can (ValueError, not a bare except:) so unrelated bugs don’t get hidden.
  • Never leave an except block empty (except Exception: pass) — at minimum log the error, since silent failures are extremely hard to debug later.
  • Use else to keep code that should only run on success out of the try block, so you don’t accidentally catch exceptions raised by that follow-up code too.
  • Prefer context managers (with open(...) as f:) over manual try/finally for resource cleanup when the object supports it — it’s shorter and equally safe.
  • Avoid return, break, or continue inside a finally block; they silently swallow any exception in flight.
  • Use raise NewError(...) from original when translating a low-level exception into a higher-level one, so the original traceback isn’t lost.
  • Don’t use exceptions for routine control flow where a simple if check is clearer and cheaper — reserve them for genuinely exceptional situations.
  • Define custom exception classes for your own domain errors instead of raising generic Exception, so callers can catch precisely what they expect.

Practice Exercises

  1. Write a function convert_to_float(value) that tries to convert value to a float and return it. If a ValueError occurs, print a message and return 0.0 instead. Use finally to print "Conversion attempt complete" every time, whether or not the conversion succeeded.
  2. Write a function read_first_item(sequence) that returns sequence[0]. Catch IndexError for an empty sequence and TypeError for a non-indexable argument (like an int), printing a distinct message for each. Use else to print the retrieved item only when no exception occurred.
  3. Create a custom exception class NegativeValueError and a function square_root(x) that raises it when x is negative, and otherwise returns x ** 0.5. Call it inside a try/except that catches NegativeValueError and prints a friendly message, with a finally block that always prints "Calculation attempt finished".

Summary

  • try wraps code that might raise an exception; except handles specific exception types when they occur.
  • else runs only when the try block completes with no exception, keeping success-path code separate from the risky code.
  • finally always runs — on success, on a caught exception, on an uncaught exception, and even across return/break/continue — making it the right place for guaranteed cleanup.
  • A return or raise placed inside finally silently overrides whatever the try/except was doing — avoid it.
  • Catch specific exception types rather than using a bare except:, which also catches KeyboardInterrupt and SystemExit.
  • Custom exception classes (subclassing Exception) let you model domain-specific failure cases clearly for callers.
  • Use raise ... from ... to chain exceptions and preserve the original cause when translating errors.