The four-clause anatomy of try/except/else/finally
A full exception block has four parts, used in this exact order. try wraps the risky code. except runs only if a matching exception fires inside try. else runs only if try completed without an exception. finally runs always, exception or not, and is the place to release resources.
The else clause is the part most students skip and most TAs reward. Code that should only run after a successful try belongs in else, not at the bottom of try. Putting it in try means a later exception (raised by the success path) gets caught by the same except, which masks the real bug. Putting it in else makes the boundary explicit: try contains exactly the line that may raise, else contains everything that runs only on success.
finally always runs. Use it for cleanup that must happen regardless of outcome: closing a network connection, restoring a working directory, releasing a lock. Context managers (with statements) cover the most common cases automatically, but finally is the explicit form when no context manager exists.
def divide_safely(numerator, denominator):
try:
result = numerator / denominator # the risky operation
except ZeroDivisionError: # only runs on this specific failure
print('Cannot divide by zero')
return None
except TypeError: # different failure, different handling
print('Both arguments must be numbers')
return None
else: # only runs if try succeeded with no exception
print(f'Division succeeded: {result}')
return result
finally: # always runs, success or failure
print('divide_safely() finished')
# Three calls covering all three paths
divide_safely(10, 2) # success path: else and finally both run
divide_safely(10, 0) # ZeroDivisionError path
divide_safely(10, 'x') # TypeError path